13 Commits

24 changed files with 447 additions and 461 deletions

View File

@ -15,8 +15,8 @@ go get git.streifling.com/jason/atom@latest
## Usage
This library provides convenient functions to safely create and extend elements
and attributes of an Atom feed. This is because it can be hard to know all
pitfalls of RFC4287. The intended way of using atom is with these functions.
and attributes of Atom feeds. It also provides checks for all constructs'
adherence to RFC4287.
```go
package main
@ -29,30 +29,25 @@ import (
)
func main() {
feed, err := atom.NewFeed("Example Feed")
if err != nil {
feed := atom.NewFeed("Example Feed")
if err := feed.Check(); err != nil {
log.Fatalln(err)
}
author := atom.NewPerson("John Doe")
author.Email = "john.doe@example.com"
if err := author.Check(); err != nil {
log.Fatalln(err)
}
feed.AddAuthor(author)
entry, err := atom.NewEntry("First Entry")
if err != nil {
entry := atom.NewEntry("First Entry")
entry.Content = atom.NewContent(atom.InlineText, "text", "This is the content of the first entry.")
if err := entry.Check(); err != nil {
log.Fatalln(err)
}
content, err := atom.NewContent(atom.InlineText, "text", "This is the content of the first entry.")
if err != nil {
log.Fatalln(err)
}
entry.Content = content
feed.AddEntry(entry)
if err := feed.Check(); err != nil {
log.Fatalln(err)
}
feedString, err := feed.ToXML("utf-8")
if err != nil {
log.Fatalln(err)
@ -62,7 +57,7 @@ func main() {
```
It is also possible to use this library in a way similar to what other libraries
would provide. This is, of course, making it easier to make mistakes.
provide.
```go
package main
@ -115,7 +110,7 @@ func main() {
}
```
The output of both ways of using it is an RFC4287 compliant Atom feed:
The output of both ways of using it is an RFC4287 compliant Atom feed.
```xml
<?xml version="1.0" encoding="utf-8"?>

31
atom.go
View File

@ -1,7 +1,9 @@
package atom
import (
"encoding/xml"
"fmt"
"html"
"mime"
"regexp"
"strings"
@ -10,6 +12,30 @@ import (
"golang.org/x/text/language"
)
type Countable interface {
*xml.Attr | *Person | *Category | *Link | *ExtensionElement | *Entry
}
// addToSlice adds a Countable to to a *[]Countable
func addToSlice[C Countable](slice *[]C, countable C) {
if *slice == nil {
*slice = make([]C, 0)
}
*slice = append(*slice, countable)
}
// deleteFromSlice deletes the Countable with the index from the *[]Countable.
// It return an error.
func deleteFromSlice[C Countable](slice *[]C, index int) error {
length := len(*slice)
if index > length {
return fmt.Errorf("id %v out of range %v", index, length)
}
*slice = append((*slice)[:index], (*slice)[index+1:]...)
return nil
}
// isValidIRI checks whether an IRI is valid or not. It returns a bool.
// https://www.w3.org/2011/04/XMLSchema/TypeLibrary-IRI-RFC3987.xsd
func isValidIRI(iri string) bool {
@ -85,3 +111,8 @@ func isValidAttribute(attribute string) bool {
func NewURN() string {
return fmt.Sprint("urn:uuid:", uuid.New())
}
// Unescape unescapes a string. It returns an IRI.
func Unescape(s string) string {
return html.UnescapeString(s)
}

View File

@ -2,9 +2,7 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
"html"
)
type Category struct {
@ -15,43 +13,17 @@ type Category struct {
Label string `xml:"label,attr,omitempty"`
}
// NewCategory creates a new Category. It returns a *Category and an error.
func NewCategory(term string) (*Category, error) {
if term == "" {
return nil, errors.New("error creating new category: term string empty")
// NewCategory creates a new Category. It returns a *Category.
func NewCategory(term string) *Category {
return &Category{
CommonAttributes: newCommonAttributes(),
Term: term,
}
}
return &Category{Term: term}, nil
}
// SetTerm sets the Term attribute of the Category. It returns an error.
func (c *Category) SetTerm(t string) error {
if t == "" {
return errors.New("error setting term of category: t string empty")
}
c.Term = t
return nil
}
// SetScheme sets the Scheme attribute of the Category. It returns an error.
func (c *Category) SetScheme(s string) error {
if !isValidIRI(s) {
return fmt.Errorf("scheme %v not correctly formatted", s)
}
c.Scheme = s
return nil
}
// SetLabel sets the Label attribute of the Category. It returns an error.
func (c *Category) SetLabel(label string) error {
if label == "" {
return errors.New("error setting label of category: label string empty")
}
c.Label = html.UnescapeString(label)
return nil
// SetLabel sets the Label attribute of the Category.
func (c *Category) SetLabel(label string) {
c.Label = Unescape(label)
}
// Check checks the Category for incompatibilities with RFC4287. It returns an

View File

@ -2,7 +2,7 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
)
type CommonAttributes struct {
@ -13,24 +13,34 @@ type CommonAttributes struct {
// NewCommonAttributes creates a new set of CommonAttributes. It returns a
// *CommonAttributes.
func NewCommonAttributes() *CommonAttributes {
func newCommonAttributes() *CommonAttributes {
return new(CommonAttributes)
}
// AddAttribute adds the Attribute to the CommonAttributes. It returns an error.
func (c *CommonAttributes) AddAttribute(name, value string) error {
if name == "" {
return errors.New("error adding attribute: name string empty")
}
if value == "" {
return errors.New("error adding attribute: value string empty")
// AddAttribute adds the attribute to the CommonAttributes.
func (c *CommonAttributes) AddAttribute(name, value string) {
addToSlice(&c.UndefinedAttributes, &xml.Attr{Name: xml.Name{Local: name}, Value: value})
}
if c.UndefinedAttributes == nil {
c.UndefinedAttributes = make([]*xml.Attr, 1)
c.UndefinedAttributes[0] = &xml.Attr{Name: xml.Name{Local: name}, Value: value}
} else {
c.UndefinedAttributes = append(c.UndefinedAttributes, &xml.Attr{Name: xml.Name{Local: name}, Value: value})
// DeleteAttribute deletes the attribute at index from the CommonAttributes. It
// return an error.
func (c *CommonAttributes) DeleteAttribute(index int) error {
if err := deleteFromSlice(&c.UndefinedAttributes, index); err != nil {
return fmt.Errorf("error deleting undefined attribute %v from common attributes %v: %v", index, c, err)
}
return nil
}
// Check checks the CommonAttributes for incompatibilities with RFC4287. It
// returns an error.
func (c *CommonAttributes) Check() error {
for i, u := range c.UndefinedAttributes {
if u.Name.Local == "" {
return fmt.Errorf("xml name of undefined attribute %v empty", i)
}
if u.Value == "" {
return fmt.Errorf("value of undefined attribute %v empty", i)
}
}
return nil

View File

@ -1,7 +1,5 @@
package atom
import "fmt"
const (
InlineText = iota
InlineXHTML
@ -17,17 +15,17 @@ type Content interface {
}
// NewContent creates a new Content. It returns a Content and an error.
func NewContent(contentType int, mediaType string, content any) (Content, error) {
func NewContent(contentType int, mediaType string, content any) Content {
switch contentType {
case 0:
return newInlineTextContent(mediaType, content)
return newInlineTextContent(mediaType, content.(string))
case 1:
return newInlineXHTMLContent(mediaType, content)
return newInlineXHTMLContent(mediaType, content.(*XHTMLDiv))
case 2:
return newInlineOtherContent(mediaType, content)
case 3:
return newOutOfLineContent(mediaType, content)
return newOutOfLineContent(mediaType, content.(string))
default:
return nil, fmt.Errorf("error creating new content: %v is not a valid text type", contentType)
return nil
}
}

View File

@ -18,7 +18,10 @@ func DateTime(t time.Time) string {
// NewDate creates a new Date. It returns a *Date.
func NewDate(t time.Time) *Date {
return &Date{DateTime: DateTime(t)}
return &Date{
CommonAttributes: newCommonAttributes(),
DateTime: DateTime(t),
}
}
// Check checks the Date for incompatibilities with RFC4287. It returns an

177
entry.go
View File

@ -2,7 +2,6 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
"strings"
"time"
@ -40,16 +39,16 @@ func (e *Entry) checkAuthors(authorInFeed bool) error {
if e.Authors == nil {
if !authorInFeed {
if e.Source == nil {
return fmt.Errorf("no authors set in entry %v", e)
return fmt.Errorf("no authors set in entry %v", e.ID.URI)
}
if e.Source.Authors == nil {
return fmt.Errorf("no authors set in entry %v", e)
return fmt.Errorf("no authors set in entry %v", e.ID.URI)
}
}
} else {
for i, a := range e.Authors {
if err := a.Check(); err != nil {
return fmt.Errorf("author element %v of entry %v: %v", i, e, err)
return fmt.Errorf("author element %v of entry %v: %v", i, e.ID.URI, err)
}
}
}
@ -57,108 +56,96 @@ func (e *Entry) checkAuthors(authorInFeed bool) error {
return nil
}
// NewEntry creates a new Entry. It returns a *Entry and an error.
func NewEntry(title string) (*Entry, error) {
text, err := NewText("text", title)
if err != nil {
return nil, fmt.Errorf("error creating new entry: %v", err)
}
id, err := NewID(NewURN())
if err != nil {
return nil, fmt.Errorf("error creating new entry: %v", err)
}
// NewEntry creates a new Entry. It returns a *Entry.
func NewEntry(title string) *Entry {
return &Entry{
ID: id,
Title: text,
CommonAttributes: newCommonAttributes(),
ID: NewID(NewURN()),
Title: NewText("text", title),
Updated: NewDate(time.Now()),
}, nil
}
}
// AddAuthor adds the Person as an author to the Entry. It returns an error.
func (e *Entry) AddAuthor(p *Person) error {
if p == nil {
return errors.New("error adding author element to entry: *Person is nil")
// AddAuthor adds the Person as an author to the Entry.
func (e *Entry) AddAuthor(p *Person) {
addToSlice(&e.Authors, p)
e.Updated = NewDate(time.Now())
}
if e.Authors == nil {
e.Authors = make([]*Person, 1)
e.Authors[0] = p
} else {
e.Authors = append(e.Authors, p)
// DeleteAuthor deletes the Person at index from the Entry. It return an error.
func (e *Entry) DeleteAuthor(index int) error {
if err := deleteFromSlice(&e.Authors, index); err != nil {
return fmt.Errorf("error deleting author %v from entry %v: %v", index, e.ID.URI, err)
}
e.Updated.DateTime = DateTime(time.Now())
e.Updated = NewDate(time.Now())
return nil
}
// AddCategory adds the Category to the Entry. It returns an error.
func (e *Entry) AddCategory(c *Category) error {
if c == nil {
return errors.New("error adding category element to entry: *Category is nil")
// AddCategory adds the Category to the Entry.
func (e *Entry) AddCategory(c *Category) {
addToSlice(&e.Categories, c)
e.Updated = NewDate(time.Now())
}
if e.Categories == nil {
e.Categories = make([]*Category, 1)
e.Categories[0] = c
} else {
e.Categories = append(e.Categories, c)
}
e.Updated.DateTime = DateTime(time.Now())
return nil
}
// AddContributor adds the Person as a contributor to the Entry. It returns an
// DeleteCategory deletes the Category at index from the Entry. It return an
// error.
func (e *Entry) AddContributor(c *Person) error {
if c == nil {
return errors.New("error adding contributor element to entry: *Person is nil")
func (e *Entry) DeleteCategory(index int) error {
if err := deleteFromSlice(&e.Categories, index); err != nil {
return fmt.Errorf("error deleting category %v from entry %v: %v", index, e.ID.URI, err)
}
if e.Contributors == nil {
e.Contributors = make([]*Person, 1)
e.Contributors[0] = c
} else {
e.Contributors = append(e.Contributors, c)
}
e.Updated.DateTime = DateTime(time.Now())
e.Updated = NewDate(time.Now())
return nil
}
// AddLink adds the Link to the Entry. It returns an error.
func (e *Entry) AddLink(l *Link) error {
if l == nil {
return errors.New("error adding link element to entry: *Link is nil")
// AddContributor adds the Person as a contributor to the Entry.
func (e *Entry) AddContributor(c *Person) {
addToSlice(&e.Contributors, c)
e.Updated = NewDate(time.Now())
}
if e.Links == nil {
e.Links = make([]*Link, 1)
e.Links[0] = l
} else {
e.Links = append(e.Links, l)
// DeleteContributor deletes the Person at index from the Entry. It return an
// error.
func (e *Entry) DeleteContributor(index int) error {
if err := deleteFromSlice(&e.Contributors, index); err != nil {
return fmt.Errorf("error deleting contributor %v from entry %v: %v", index, e.ID.URI, err)
}
e.Updated.DateTime = DateTime(time.Now())
e.Updated = NewDate(time.Now())
return nil
}
// AddExtension adds the ExtensionElement to the Entry. It returns an error.
func (e *Entry) AddExtension(x *ExtensionElement) error {
if x == nil {
return errors.New("error adding extension element to entry: *ExtensionElement is nil")
// AddLink adds the Link to the Entry.
func (e *Entry) AddLink(l *Link) {
addToSlice(&e.Links, l)
e.Updated = NewDate(time.Now())
}
if e.Extensions == nil {
e.Extensions = make([]*ExtensionElement, 1)
e.Extensions[0] = x
} else {
e.Extensions = append(e.Extensions, x)
// DeleteLink deletes the Link at index from the Entry. It return an error.
func (e *Entry) DeleteLink(index int) error {
if err := deleteFromSlice(&e.Links, index); err != nil {
return fmt.Errorf("error deleting link %v from entry %v: %v", index, e.ID.URI, err)
}
e.Updated.DateTime = DateTime(time.Now())
e.Updated = NewDate(time.Now())
return nil
}
// AddExtension adds the ExtensionElement to the Entry.
func (e *Entry) AddExtension(x *ExtensionElement) {
addToSlice(&e.Extensions, x)
e.Updated = NewDate(time.Now())
}
// DeleteExtension deletes the Extension at index from the Entry. It return an
// error.
func (e *Entry) DeleteExtension(index int) error {
if err := deleteFromSlice(&e.Extensions, index); err != nil {
return fmt.Errorf("error deleting extension %v from entry %v: %v", index, e.ID.URI, err)
}
e.Updated = NewDate(time.Now())
return nil
}
@ -174,64 +161,64 @@ func (e *Entry) Check() error {
}
if err := e.checkAuthors(true); err != nil {
return fmt.Errorf("entry %v: %v", e, err)
return fmt.Errorf("entry %v: %v", e.ID.URI, err)
}
for i, c := range e.Categories {
if err := c.Check(); err != nil {
return fmt.Errorf("category element %v of entry %v: %v", i, e, err)
return fmt.Errorf("category element %v of entry %v: %v", i, e.ID.URI, err)
}
}
if e.Content != nil {
if err := e.Content.Check(); err != nil {
return fmt.Errorf("content element of entry %v: %v", e, err)
return fmt.Errorf("content element of entry %v: %v", e.ID.URI, err)
}
} else {
// atom:entry elements that contain no child atom:content element MUST
// contain at least one atom:link element with a rel attribute value of
// "alternate".
if !alternateRelExists(e.Links) {
return fmt.Errorf("no content element of entry %v and no link element with rel \"alternate\"", e)
return fmt.Errorf("no content element of entry %v and no link element with rel \"alternate\"", e.ID.URI)
}
}
for i, c := range e.Contributors {
if err := c.Check(); err != nil {
return fmt.Errorf("contributor element %v of entry %v: %v", i, e, err)
return fmt.Errorf("contributor element %v of entry %v: %v", i, e.ID.URI, err)
}
}
for i, l := range e.Links {
if err := l.Check(); err != nil {
return fmt.Errorf("link element %v of entry %v: %v", i, e, err)
return fmt.Errorf("link element %v of entry %v: %v", i, e.ID.URI, err)
}
}
if hasAlternateDuplicateLinks(e.Links) {
return fmt.Errorf("links with a rel attribute value of \"alternate\" and duplicate type and hreflang attribute values found in entry %v", e)
return fmt.Errorf("links with a rel attribute value of \"alternate\" and duplicate type and hreflang attribute values found in entry %v", e.ID.URI)
}
if e.Published != nil {
if err := e.Published.Check(); err != nil {
return fmt.Errorf("published element of entry %v: %v", e, err)
return fmt.Errorf("published element of entry %v: %v", e.ID.URI, err)
}
}
if e.Rights != nil {
if err := e.Rights.Check(); err != nil {
return fmt.Errorf("rights element of entry %v: %v", e, err)
return fmt.Errorf("rights element of entry %v: %v", e.ID.URI, err)
}
}
if e.Source != nil {
if err := e.Source.Check(); err != nil {
return fmt.Errorf("source element of entry %v: %v", e, err)
return fmt.Errorf("source element of entry %v: %v", e.ID.URI, err)
}
}
if e.Summary != nil {
if err := e.Summary.Check(); err != nil {
return fmt.Errorf("summary element of entry %v: %v", e, err)
return fmt.Errorf("summary element of entry %v: %v", e.ID.URI, err)
}
} else {
// atom:entry elements MUST contain an atom:summary element in either
@ -239,7 +226,7 @@ func (e *Entry) Check() error {
// the atom:entry contains an atom:content that has a "src" attribute
// (and is thus empty).
if e.Content.hasSRC() {
return fmt.Errorf("no summary element of entry %v but content of type out of line content", e)
return fmt.Errorf("no summary element of entry %v but content of type out of line content", e.ID.URI)
}
// the atom:entry contains content that is encoded in Base64; i.e., the
// "type" attribute of atom:content is a MIME media type [MIMEREG], but
@ -247,29 +234,29 @@ func (e *Entry) Check() error {
// does not end with "/xml" or "+xml".
mediaType := e.Content.getType()
if isValidMediaType(mediaType) && !isXMLMediaType(mediaType) && !strings.HasPrefix(mediaType, "text/") {
return fmt.Errorf("no summary element of entry %v but media type not xml", e)
return fmt.Errorf("no summary element of entry %v but media type not xml", e.ID.URI)
}
}
if e.Title == nil {
return fmt.Errorf("no title element of entry %v", e)
return fmt.Errorf("no title element of entry %v", e.ID.URI)
} else {
if err := e.Title.Check(); err != nil {
return fmt.Errorf("title element of entry %v: %v", e, err)
return fmt.Errorf("title element of entry %v: %v", e.ID.URI, err)
}
}
if e.Updated == nil {
return fmt.Errorf("no updated element of entry %v", e)
return fmt.Errorf("no updated element of entry %v", e.ID.URI)
} else {
if err := e.Updated.Check(); err != nil {
return fmt.Errorf("updated element of entry %v: %v", e, err)
return fmt.Errorf("updated element of entry %v: %v", e.ID.URI, err)
}
}
for i, x := range e.Extensions {
if err := x.Check(); err != nil {
return fmt.Errorf("extension element %v of entry %v: %v", i, e, err)
return fmt.Errorf("extension element %v of entry %v: %v", i, e.ID.URI, err)
}
}
@ -280,7 +267,7 @@ func (e *Entry) Check() error {
func (e *Entry) ToXML(encoding string) (string, error) {
xml, err := xml.MarshalIndent(e, "", " ")
if err != nil {
return "", fmt.Errorf("error xml encoding entry: %v", err)
return "", fmt.Errorf("error xml encoding entry %v: %v", e.ID.URI, err)
}
return fmt.Sprintln(`<?xml version="1.0" encoding="`+encoding+`"?>`) + string(xml), nil

View File

@ -2,7 +2,7 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
)
type ExtensionElement struct {
@ -11,27 +11,20 @@ type ExtensionElement struct {
}
// NewExtensionElement creates a new ExtensionElement. It returns a
// *ExtensionElement and an error.
func NewExtensionElement(name string, value any) (*ExtensionElement, error) {
if name == "" {
return nil, errors.New("error adding extension attribute: name string empty")
}
if value == "" {
return nil, errors.New("error adding extension attribute: value string empty")
}
return &ExtensionElement{XMLName: xml.Name{Local: name}, Value: value}, nil
// *ExtensionElement.
func NewExtensionElement(name string, value any) *ExtensionElement {
return &ExtensionElement{XMLName: xml.Name{Local: name}, Value: value}
}
// Check checks the ExtensionElement for incompatibilities with RFC4287. It
// returns an error.
func (e *ExtensionElement) Check() error {
if e.XMLName.Local == "" {
return errors.New("xml name of extension empty")
return fmt.Errorf("xml name of extension %v empty", e)
}
if e.Value == nil {
return errors.New("value element of extension empty")
return fmt.Errorf("value of extension %v empty", e)
}
return nil

211
feed.go
View File

@ -2,7 +2,6 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
"time"
)
@ -26,126 +25,134 @@ type Feed struct {
Entries []*Entry `xml:",omitempty"`
}
// NewFeed creates a new Feed. It returns a *Feed and an error.
func NewFeed(title string) (*Feed, error) {
text, err := NewText("text", title)
if err != nil {
return nil, fmt.Errorf("error creating new feed: %v", err)
}
id, err := NewID(NewURN())
if err != nil {
return nil, fmt.Errorf("error creating new feed: %v", err)
}
// NewFeed creates a new Feed. It returns a *Feed.
func NewFeed(title string) *Feed {
return &Feed{
ID: id,
Title: text,
CommonAttributes: newCommonAttributes(),
ID: NewID(NewURN()),
Title: NewText("text", title),
Updated: NewDate(time.Now()),
}, nil
}
}
// AddAuthor adds the Person as an author to the Feed. It returns an error.
func (f *Feed) AddAuthor(p *Person) error {
if p == nil {
return errors.New("error adding author element to feed: *Person is nil")
// AddAuthor adds the Person as an author to the Feed.
func (f *Feed) AddAuthor(p *Person) {
addToSlice(&f.Authors, p)
f.Updated = NewDate(time.Now())
}
if f.Authors == nil {
f.Authors = make([]*Person, 1)
f.Authors[0] = p
} else {
f.Authors = append(f.Authors, p)
// DeleteAuthor deletes the Person at index from the Feed. It return an error.
func (f *Feed) DeleteAuthor(index int) error {
if err := deleteFromSlice(&f.Authors, index); err != nil {
return fmt.Errorf("error deleting author %v from entry %v: %v", index, f.ID.URI, err)
}
f.Updated.DateTime = DateTime(time.Now())
f.Updated = NewDate(time.Now())
return nil
}
// AddCategory adds the Category to the Feed. It returns an error.
func (f *Feed) AddCategory(c *Category) error {
if c == nil {
return errors.New("error adding category element to feed: *Category is nil")
// AddCategory adds the Category to the Feed.
func (f *Feed) AddCategory(c *Category) {
addToSlice(&f.Categories, c)
f.Updated = NewDate(time.Now())
}
if f.Categories == nil {
f.Categories = make([]*Category, 1)
f.Categories[0] = c
} else {
f.Categories = append(f.Categories, c)
}
f.Updated.DateTime = DateTime(time.Now())
return nil
}
// AddContributor adds the Person as a contributor to the Feed. It returns an
// DeleteCategory deletes the Category at index from the Feed. It return an
// error.
func (f *Feed) AddContributor(c *Person) error {
if c == nil {
return errors.New("error adding contributor element to feed: *Person is nil")
func (f *Feed) DeleteCategory(index int) error {
if err := deleteFromSlice(&f.Categories, index); err != nil {
return fmt.Errorf("error deleting category %v from entry %v: %v", index, f.ID.URI, err)
}
if f.Contributors == nil {
f.Contributors = make([]*Person, 1)
f.Contributors[0] = c
} else {
f.Contributors = append(f.Contributors, c)
}
f.Updated.DateTime = DateTime(time.Now())
f.Updated = NewDate(time.Now())
return nil
}
// AddLink adds the Link to the Feed. It returns an error. There should be one
// Link with Rel "self".
func (f *Feed) AddLink(l *Link) error {
if l == nil {
return errors.New("error adding link element to feed: *Link is nil")
// AddContributor adds the Person as a contributor to the Feed.
func (f *Feed) AddContributor(c *Person) {
addToSlice(&f.Contributors, c)
f.Updated = NewDate(time.Now())
}
if f.Links == nil {
f.Links = make([]*Link, 1)
f.Links[0] = l
} else {
f.Links = append(f.Links, l)
// DeleteContributor deletes the Person at index from the Feed. It return an
// error.
func (f *Feed) DeleteContributor(index int) error {
if err := deleteFromSlice(&f.Contributors, index); err != nil {
return fmt.Errorf("error deleting contributor %v from entry %v: %v", index, f.ID.URI, err)
}
f.Updated.DateTime = DateTime(time.Now())
f.Updated = NewDate(time.Now())
return nil
}
// AddExtension adds the Extension to the Feed. It returns an error.
func (f *Feed) AddExtension(e *ExtensionElement) error {
if e == nil {
return errors.New("error adding extension element to feed: *ExtensionElement is nil")
// AddLink adds the Link to the Feed. There should be one Link with Rel "self".
func (f *Feed) AddLink(l *Link) {
addToSlice(&f.Links, l)
f.Updated = NewDate(time.Now())
}
if f.Extensions == nil {
f.Extensions = make([]*ExtensionElement, 1)
f.Extensions[0] = e
} else {
f.Extensions = append(f.Extensions, e)
// DeleteLink deletes the Link at index from the Feed. It return an error.
func (f *Feed) DeleteLink(index int) error {
if err := deleteFromSlice(&f.Links, index); err != nil {
return fmt.Errorf("error deleting link %v from entry %v: %v", index, f.ID.URI, err)
}
f.Updated.DateTime = DateTime(time.Now())
f.Updated = NewDate(time.Now())
return nil
}
// AddEntry adds the Entry to the Feed. It returns an error.
func (f *Feed) AddEntry(e *Entry) error {
if e == nil {
return errors.New("error adding entry element to feed: *Entry is nil")
// AddExtension adds the Extension to the Feed.
func (f *Feed) AddExtension(e *ExtensionElement) {
addToSlice(&f.Extensions, e)
f.Updated = NewDate(time.Now())
}
if f.Entries == nil {
f.Entries = make([]*Entry, 1)
f.Entries[0] = e
} else {
f.Entries = append(f.Entries, e)
// DeleteExtension deletes the Extension at index from the Feed. It return an
// error.
func (f *Feed) DeleteExtension(index int) error {
if err := deleteFromSlice(&f.Extensions, index); err != nil {
return fmt.Errorf("error deleting extension %v from entry %v: %v", index, f.ID.URI, err)
}
f.Updated.DateTime = DateTime(time.Now())
f.Updated = NewDate(time.Now())
return nil
}
// AddEntry adds the Entry to the Feed.
func (f *Feed) AddEntry(e *Entry) {
addToSlice(&f.Entries, e)
f.Updated = NewDate(time.Now())
}
// DeleteEntry deletes the Entry at index from the Feed. It return an error.
func (f *Feed) DeleteEntry(index int) error {
if err := deleteFromSlice(&f.Entries, index); err != nil {
return fmt.Errorf("error deleting entry %v from entry %v: %v", index, f.ID.URI, err)
}
f.Updated = NewDate(time.Now())
return nil
}
// DeleteEntryByURI deletes the Entry from the Feed. It return an error.
func (f *Feed) DeleteEntryByURI(uri string) error {
if !isValidIRI(uri) {
return fmt.Errorf("error deleting entry from feed %v: uri %v invalid", f.ID.URI, uri)
}
index := -1
for i, e := range f.Entries {
if e.ID.URI == uri {
index = i
break
}
}
if index < 0 {
return fmt.Errorf("error deleting entry from feed %v: id %v not found", f.ID.URI, uri)
}
f.Entries = append(f.Entries[:index], f.Entries[index+1:]...)
f.Updated = NewDate(time.Now())
return nil
}
@ -166,93 +173,93 @@ func (f *Feed) Check() error {
if f.Authors == nil {
for _, e := range f.Entries {
if err := e.checkAuthors(false); err != nil {
return fmt.Errorf("no authors set in feed %v: %v", f, err)
return fmt.Errorf("no authors set in feed %v: %v", f.ID.URI, err)
}
}
} else {
for i, a := range f.Authors {
if err := a.Check(); err != nil {
return fmt.Errorf("author element %v of feed %v: %v", i, f, err)
return fmt.Errorf("author element %v of feed %v: %v", i, f.ID.URI, err)
}
}
}
for i, c := range f.Categories {
if err := c.Check(); err != nil {
return fmt.Errorf("category element %v of feed %v: %v", i, f, err)
return fmt.Errorf("category element %v of feed %v: %v", i, f.ID.URI, err)
}
}
for i, c := range f.Contributors {
if err := c.Check(); err != nil {
return fmt.Errorf("contributor element %v of feed %v: %v", i, f, err)
return fmt.Errorf("contributor element %v of feed %v: %v", i, f.ID.URI, err)
}
}
if f.Generator != nil {
if err := f.Generator.Check(); err != nil {
return fmt.Errorf("generator element of feed %v: %v", f, err)
return fmt.Errorf("generator element of feed %v: %v", f.ID.URI, err)
}
}
if f.Icon != nil {
if err := f.Icon.Check(); err != nil {
return fmt.Errorf("icon element of feed %v: %v", f, err)
return fmt.Errorf("icon element of feed %v: %v", f.ID.URI, err)
}
}
for i, l := range f.Links {
if err := l.Check(); err != nil {
return fmt.Errorf("link element %v of feed %v: %v", i, f, err)
return fmt.Errorf("link element %v of feed %v: %v", i, f.ID.URI, err)
}
}
if hasAlternateDuplicateLinks(f.Links) {
return fmt.Errorf("links with a rel attribute value of \"alternate\" and duplicate type and hreflang attribute values found in feed %v", f)
return fmt.Errorf("links with a rel attribute value of \"alternate\" and duplicate type and hreflang attribute values found in feed %v", f.ID.URI)
}
if f.Logo != nil {
if err := f.Logo.Check(); err != nil {
return fmt.Errorf("logo element of feed %v: %v", f, err)
return fmt.Errorf("logo element of feed %v: %v", f.ID.URI, err)
}
}
if f.Rights != nil {
if err := f.Rights.Check(); err != nil {
return fmt.Errorf("rights element of feed %v: %v", f, err)
return fmt.Errorf("rights element of feed %v: %v", f.ID.URI, err)
}
}
if f.Subtitle != nil {
if err := f.Subtitle.Check(); err != nil {
return fmt.Errorf("subtitle element of feed %v: %v", f, err)
return fmt.Errorf("subtitle element of feed %v: %v", f.ID.URI, err)
}
}
if f.Title == nil {
return fmt.Errorf("no title element of feed %v", f)
return fmt.Errorf("no title element of feed %v", f.ID.URI)
} else {
if err := f.Title.Check(); err != nil {
return fmt.Errorf("title element of feed %v: %v", f, err)
return fmt.Errorf("title element of feed %v: %v", f.ID.URI, err)
}
}
if f.Updated == nil {
return fmt.Errorf("no updated element of feed %v", f)
return fmt.Errorf("no updated element of feed %v", f.ID.URI)
} else {
if err := f.Updated.Check(); err != nil {
return fmt.Errorf("updated element of feed %v: %v", f, err)
return fmt.Errorf("updated element of feed %v: %v", f.ID.URI, err)
}
}
for i, x := range f.Extensions {
if err := x.Check(); err != nil {
return fmt.Errorf("extension element %v of feed %v: %v", i, f, err)
return fmt.Errorf("extension element %v of feed %v: %v", i, f.ID.URI, err)
}
}
for i, n := range f.Entries {
if err := n.Check(); err != nil {
return fmt.Errorf("entry element %v of feed %v: %v", i, f, err)
return fmt.Errorf("entry element %v of feed %v: %v", i, f.ID.URI, err)
}
}
@ -263,7 +270,7 @@ func (f *Feed) Check() error {
func (f *Feed) ToXML(encoding string) (string, error) {
xml, err := xml.MarshalIndent(f, "", " ")
if err != nil {
return "", fmt.Errorf("error xml encoding feed: %v", err)
return "", fmt.Errorf("error xml encoding feed %v: %v", f.ID.URI, err)
}
return fmt.Sprintln(`<?xml version="1.0" encoding="`+encoding+`"?>`) + string(xml), nil

View File

@ -2,7 +2,6 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
"html"
)
@ -15,22 +14,12 @@ type Generator struct {
Text string `xml:",chardata"`
}
// NewGenerator creates a new Generator. It returns a *Generator and an error.
func NewGenerator(text string) (*Generator, error) {
if text == "" {
return nil, errors.New("error creating new generator: text string empty")
// NewGenerator creates a new Generator. It returns a *Generator.
func NewGenerator(text string) *Generator {
return &Generator{
CommonAttributes: newCommonAttributes(),
Text: html.UnescapeString(text),
}
return &Generator{Text: html.UnescapeString(text)}, nil
}
// SetURI sets the URI attribute of the Generator. It returns an error.
func (g *Generator) SetURI(uri string) error {
if !isValidIRI(uri) {
return fmt.Errorf("uri %v not correctly formatted", g)
}
return nil
}
// Check checks the Generator for incompatibilities with RFC4287. It returns an

11
icon.go
View File

@ -12,13 +12,12 @@ type Icon struct {
URI string `xml:",chardata"` // IRI
}
// NewIcon creates a new Icon. It returns a *Icon and an error.
func NewIcon(uri string) (*Icon, error) {
if !isValidIRI(uri) {
return nil, fmt.Errorf("uri %v not correctly formatted", uri)
// NewIcon creates a new Icon. It returns a *Icon.
func NewIcon(uri string) *Icon {
return &Icon{
CommonAttributes: newCommonAttributes(),
URI: uri,
}
return &Icon{URI: uri}, nil
}
// Check checks the Icon for incompatibilities with RFC4287. It returns an

11
id.go
View File

@ -12,13 +12,12 @@ type ID struct {
URI string `xml:",chardata"` // IRI
}
// NewID creates a new ID. It returns a *ID and an error.
func NewID(uri string) (*ID, error) {
if !isValidIRI(uri) {
return nil, fmt.Errorf("uri %v not correctly formatted", uri)
// NewID creates a new ID. It returns a *ID.
func NewID(uri string) *ID {
return &ID{
CommonAttributes: newCommonAttributes(),
URI: uri,
}
return &ID{URI: uri}, nil
}
// Check checks the ID for incompatibilities with RFC4287. It returns an error.

View File

@ -15,13 +15,14 @@ type InlineOtherContent struct {
// newInlineOtherContent creates a new InlineOtherContent. It returns a
// *InlineOtherContent and an error.
func newInlineOtherContent(mediaType string, content any) (*InlineOtherContent, error) {
if !isValidMediaType(mediaType) {
return nil, fmt.Errorf("error creating new inline other content: media type %v invalid", mediaType)
}
func newInlineOtherContent(mediaType string, content any) *InlineOtherContent {
mediaType, _, _ = mime.ParseMediaType(mediaType)
return &InlineOtherContent{Type: mediaType, AnyElement: content}, nil
return &InlineOtherContent{
CommonAttributes: newCommonAttributes(),
Type: mediaType,
AnyElement: content,
}
}
// isContent checks whether the InlineOtherContent is a Content. It returns a

View File

@ -13,18 +13,13 @@ type InlineTextContent struct {
}
// newInlineTextContent creates a new InlineTextContent. It returns a
// *InlineTextContent and an error.
func newInlineTextContent(mediaType string, content any) (*InlineTextContent, error) {
if mediaType != "text" && mediaType != "html" && mediaType != "" {
return nil, fmt.Errorf("media type %v incompatible with inline text content", mediaType)
// *InlineTextContent.
func newInlineTextContent(mediaType, text string) *InlineTextContent {
return &InlineTextContent{
CommonAttributes: newCommonAttributes(),
Type: mediaType,
Text: text,
}
text, ok := content.(string)
if !ok {
return nil, fmt.Errorf("content type %T incompatible with inline text content", content)
}
return &InlineTextContent{Type: mediaType, Text: text}, nil
}
// isContent checks whether the InlineTextContent is a Content. It returns a

View File

@ -13,18 +13,13 @@ type InlineXHTMLContent struct {
}
// newInlineXHTMLContent creates a new InlineXHTMLContent. It returns a
// *InlineXHTMLContent and an error.
func newInlineXHTMLContent(mediaType string, content any) (*InlineXHTMLContent, error) {
if mediaType != "xhtml" {
return nil, fmt.Errorf("media type %v incompatible with inline xhtml content", mediaType)
// *InlineXHTMLContent.
func newInlineXHTMLContent(mediaType string, div *XHTMLDiv) *InlineXHTMLContent {
return &InlineXHTMLContent{
CommonAttributes: newCommonAttributes(),
Type: mediaType,
XHTMLDiv: div,
}
xhtmlDiv, ok := content.(*XHTMLDiv)
if !ok {
return nil, fmt.Errorf("content type %T incompatible with inline xhtml content", content)
}
return &InlineXHTMLContent{Type: mediaType, XHTMLDiv: xhtmlDiv}, nil
}
// isContent checks whether the InlineXHTMLContent is a Content. It returns a

29
link.go
View File

@ -17,31 +17,12 @@ type Link struct {
Length uint `xml:"length,attr,omitempty"`
}
// NewLink creates a new Link. It returns a *Link and an error.
func NewLink(href string) (*Link, error) {
if !isValidIRI(href) {
return nil, fmt.Errorf("href %v not correctly formatted", href)
// NewLink creates a new Link. It returns a *Link.
func NewLink(href string) *Link {
return &Link{
CommonAttributes: newCommonAttributes(),
Href: href,
}
return &Link{Href: href}, nil
}
// SetType sets the Type attribute of the Link. It returns an error.
func (l *Link) SetType(t string) error {
if !isValidMediaType(t) {
return fmt.Errorf("type %v invalid media type", l)
}
return nil
}
// SetHrefLang sets the HrefLang attribute of the Link. It returns an error.
func (l *Link) SetHrefLang(h string) error {
if !isValidLanguageTag(h) {
return fmt.Errorf("hreflang %v invalid language tag", l)
}
return nil
}
// Check checks the Link for incompatibilities with RFC4287. It returns an

11
logo.go
View File

@ -11,13 +11,12 @@ type Logo struct {
URI string `xml:",chardata"` // IRI
}
// NewLogo creates a new Logo. It returns a *Logo and an error.
func NewLogo(uri string) (*Logo, error) {
if !isValidIRI(uri) {
return nil, fmt.Errorf("uri %v not correctly formatted", uri)
// NewLogo creates a new Logo. It returns a *Logo.
func NewLogo(uri string) *Logo {
return &Logo{
CommonAttributes: newCommonAttributes(),
URI: uri,
}
return &Logo{URI: uri}, nil
}
// Check checks the Logo for incompatibilities with RFC4287. It returns an

View File

@ -14,23 +14,15 @@ type OutOfLineContent struct {
}
// newOutOfLineContent creates a new OutOfLineContent. It returns a
// *OutOfLineContent and an error.
func newOutOfLineContent(mediaType string, content any) (*OutOfLineContent, error) {
if !isValidMediaType(mediaType) {
return nil, fmt.Errorf("error creating new out of line content: media type %v invalid", mediaType)
}
// *OutOfLineContent.
func newOutOfLineContent(mediaType, src string) *OutOfLineContent {
mediaType, _, _ = mime.ParseMediaType(mediaType)
iri, ok := content.(string)
if !ok {
return nil, fmt.Errorf("content type %T incompatible with out of line content", content)
return &OutOfLineContent{
CommonAttributes: newCommonAttributes(),
Type: mediaType,
SRC: src,
}
if !isValidIRI(iri) {
return nil, fmt.Errorf("content %v not a valid uri", iri)
}
return &OutOfLineContent{Type: mediaType, SRC: iri}, nil
}
// isContent checks whether the OutOfLineContent is a Content. It returns a

View File

@ -1,7 +1,6 @@
package atom
import (
"errors"
"fmt"
"net/mail"
)
@ -14,37 +13,25 @@ type Person struct {
Extensions []*ExtensionElement `xml:",any,omitempty"`
}
// NewPerson creates a new Person. It returns a *Person and an error.
func NewPerson(name string) (*Person, error) {
if name == "" {
return nil, errors.New("error creating new person: name string empty")
// NewPerson creates a new Person. It returns a *Person.
func NewPerson(name string) *Person {
return &Person{
CommonAttributes: newCommonAttributes(),
Name: name,
}
}
return &Person{Name: name}, nil
// AddExtension adds the Extension to the Person.
func (p *Person) AddExtension(e *ExtensionElement) {
addToSlice(&p.Extensions, e)
}
// SetURI sets the URI element of the Person. It returns an error.
func (l *Link) SetURI(uri string) error {
if !isValidIRI(uri) {
return fmt.Errorf("uri %v not correctly formatted", uri)
// DeleteExtension deletes the Extension at index from the Person. It return an
// error.
func (p *Person) DeleteExtension(index int) error {
if err := deleteFromSlice(&p.Extensions, index); err != nil {
return fmt.Errorf("error deleting extension %v from person %v: %v", index, p, err)
}
return nil
}
// AddExtension adds the Extension to the Person. It returns an error.
func (p *Person) AddExtension(e *ExtensionElement) error {
if e == nil {
return errors.New("error adding extension element to person: *ExtensionElement is nil")
}
if p.Extensions == nil {
p.Extensions = make([]*ExtensionElement, 1)
p.Extensions[0] = e
} else {
p.Extensions = append(p.Extensions, e)
}
return nil
}
@ -67,13 +54,11 @@ func (p *Person) Check() error {
}
}
if p.Extensions != nil {
for i, e := range p.Extensions {
if err := e.Check(); err != nil {
return fmt.Errorf("extension element %v of person %v: %v", i, p, err)
}
}
}
return nil
}

View File

@ -1,7 +1,6 @@
package atom
import (
"errors"
"fmt"
)
@ -14,13 +13,13 @@ type PlainText struct {
// isText checks whether the PlainText is a Text. It returns a bool.
func (p *PlainText) isText() bool { return true }
// newPlainText creates a new PlainText. It returns a *PlainText and an error.
func newPlainText(textType, content string) (*PlainText, error) {
if content == "" {
return nil, errors.New("error creating new plain text: content string empty")
// newPlainText creates a new PlainText. It returns a *PlainText.
func newPlainText(textType, content string) *PlainText {
return &PlainText{
CommonAttributes: newCommonAttributes(),
Type: textType,
Text: content,
}
return &PlainText{Type: textType, Text: content}, nil
}
// Check checks the PlainText for incompatibilities with RFC4287. It returns an

View File

@ -25,7 +25,75 @@ type Source struct {
// NewSource creates a new Source. It returns a *Source.
func NewSource() *Source {
return new(Source)
return &Source{CommonAttributes: newCommonAttributes()}
}
// AddAuthor adds the Person as an author to the Source.
func (s *Source) AddAuthor(p *Person) {
addToSlice(&s.Authors, p)
}
// DeleteAuthor deletes the Person at index from the Source. It return an error.
func (s *Source) DeleteAuthor(index int) error {
if err := deleteFromSlice(&s.Authors, index); err != nil {
return fmt.Errorf("error deleting author %v from source %v: %v", index, s, err)
}
return nil
}
// AddCategory adds the Category to the Source.
func (s *Source) AddCategory(c *Category) {
addToSlice(&s.Categories, c)
}
// DeleteCategory deletes the Category at index from the Source. It return an
// error.
func (s *Source) DeleteCategory(index int) error {
if err := deleteFromSlice(&s.Categories, index); err != nil {
return fmt.Errorf("error deleting category %v from source %v: %v", index, s, err)
}
return nil
}
// AddContributor adds the Person as a contributor to the Source.
func (s *Source) AddContributor(c *Person) {
addToSlice(&s.Contributors, c)
}
// DeleteContributor deletes the Person at index from the Source. It return an
// error.
func (s *Source) DeleteContributor(index int) error {
if err := deleteFromSlice(&s.Contributors, index); err != nil {
return fmt.Errorf("error deleting contributor %v from source %v: %v", index, s, err)
}
return nil
}
// AddLink adds the Link to the Source.
func (s *Source) AddLink(l *Link) {
addToSlice(&s.Links, l)
}
// DeleteLink deletes the Link at index from the Source. It return an error.
func (s *Source) DeleteLink(index int) error {
if err := deleteFromSlice(&s.Links, index); err != nil {
return fmt.Errorf("error deleting link %v from source %v: %v", index, s, err)
}
return nil
}
// AddExtension adds the ExtensionElement to the Source.
func (s *Source) AddExtension(e *ExtensionElement) {
addToSlice(&s.Extensions, e)
}
// DeleteExtension deletes the Extension at index from the Source. It return an
// error.
func (s *Source) DeleteExtension(index int) error {
if err := deleteFromSlice(&s.Extensions, index); err != nil {
return fmt.Errorf("error deleting extension %v from source %v: %v", index, s, err)
}
return nil
}
// Check checks the Source for incompatibilities with RFC4287. It returns an

11
text.go
View File

@ -1,17 +1,14 @@
package atom
import (
"fmt"
"html"
)
import "html"
type Text interface {
isText() bool
Check() error
}
// NewText creates a new Text. It returns a Text and an error.
func NewText(textType, content string) (Text, error) {
// NewText creates a new Text. It returns a Text.
func NewText(textType, content string) Text {
switch textType {
case "text", "":
return newPlainText(textType, content)
@ -20,6 +17,6 @@ func NewText(textType, content string) (Text, error) {
case "xhtml":
return newXHTMLText(textType, content)
default:
return nil, fmt.Errorf("%v is not a valid text type", textType)
return nil
}
}

View File

@ -2,7 +2,6 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
)
@ -12,16 +11,12 @@ type XHTMLDiv struct {
Content string `xml:",innerxml"`
}
// NewXHTMLDiv creates a new XHTMLDiv. It returns a *XHTMLDiv and an error.
func NewXHTMLDiv(content string) (*XHTMLDiv, error) {
if content == "" {
return nil, errors.New("error creating new xhtml div: content string empty")
}
// NewXHTMLDiv creates a new XHTMLDiv. It returns a *XHTMLDiv.
func NewXHTMLDiv(content string) *XHTMLDiv {
return &XHTMLDiv{
XMLNS: "http://www.w3.org/1999/xhtml",
Content: content,
}, nil
}
}
// Check checks the XHTMLDiv for incompatibilities with RFC4287. It returns an

View File

@ -13,17 +13,13 @@ type XHTMLText struct {
// isText checks whether the XHTMLText is a Text. It returns a bool.
func (x *XHTMLText) isText() bool { return true }
// newPlainText creates a new PlainText. It returns a *PlainText and an error.
func newXHTMLText(textType, content string) (*XHTMLText, error) {
xhtmlDiv, err := NewXHTMLDiv(content)
if err != nil {
return nil, fmt.Errorf("error creating new xhtml text: %v", err)
}
// newPlainText creates a new PlainText. It returns a *PlainText.
func newXHTMLText(textType, content string) *XHTMLText {
return &XHTMLText{
CommonAttributes: newCommonAttributes(),
Type: textType,
XHTMLDiv: xhtmlDiv,
}, nil
XHTMLDiv: NewXHTMLDiv(content),
}
}
// Check checks the XHTMLText for incompatibilities with RFC4287. It returns an