30 Commits

Author SHA1 Message Date
3172a4865a Create needed functions for common attributes and their extensions 2024-10-17 20:48:32 +02:00
3b3f1f7e41 Change title 2024-10-17 20:47:37 +02:00
9c38048bd2 Properly use xml names 2024-10-17 20:10:18 +02:00
26e0c99150 Use *XHTMLDiv 2024-10-17 19:44:27 +02:00
6117876a59 Check uris before applying them 2024-10-17 19:28:09 +02:00
cb61d90cae Make NewLogo more flexible 2024-10-17 19:19:22 +02:00
30623fdfe0 More ",chardata" 2024-10-17 19:14:37 +02:00
4ae8cecb17 More ",chardata" 2024-10-17 19:11:33 +02:00
8e226b48ef isValidXML is no longer of any use 2024-10-17 19:05:23 +02:00
478d679985 Small bug fix 2024-10-17 18:59:24 +02:00
d11b229691 No more undefined content in link and category
It seems that undefined content is only mentioned in RFC4287 because
link and category are elements and those usually need some content.
2024-10-17 18:57:43 +02:00
e73b78ef30 Content can also be empty 2024-10-17 18:40:52 +02:00
5a82f1799f Force undefined content of category and link to be empty or valid xml 2024-10-17 18:39:57 +02:00
73624eadd8 More ",chardata" 2024-10-17 18:15:41 +02:00
17aa4b1a15 Streamline type check in content constructs 2024-10-17 18:12:56 +02:00
040d7a6b7b Create xhtmlDiv.go 2024-10-17 18:12:23 +02:00
705b651f08 Make undefined content in category and link type string 2024-10-17 18:11:06 +02:00
4b97bf7fdc Simplify inline text content 2024-10-17 17:33:33 +02:00
86785be588 More ",chardata" 2024-10-17 17:19:43 +02:00
28f5616f76 Make uri in id and text in plain text xml type ",chardata" 2024-10-17 17:14:10 +02:00
657624cbd6 Small bug fix 2024-10-17 16:56:46 +02:00
b496ac3691 Allow the user to generate a valid iri based on a uuid 2024-10-17 16:52:03 +02:00
13e7a16178 Let the user define their own iri 2024-10-17 16:51:40 +02:00
3734a3eb3d More complete check within isValidMediaType 2024-10-17 16:46:40 +02:00
e0902d9ba4 Fix source check in entry 2024-10-17 05:34:32 +02:00
434783165b Add needed checks for link if attributes are empty 2024-10-17 05:18:02 +02:00
46138d302b Fix bug in link check 2024-10-17 05:13:17 +02:00
6b9d5be734 Act as if there is an author in the feed when only checking the entry 2024-10-17 05:05:14 +02:00
2ef8d8c9df Fix nil dereference bug in entry checkAuthors 2024-10-17 04:55:28 +02:00
97fe20f364 Add missing methods to entry 2024-10-16 21:35:24 +02:00
22 changed files with 261 additions and 138 deletions

View File

@ -1,4 +1,3 @@
# atom-feed # atom
An extensible implementation of an Atom feed that aims to be very close to RFC4287. An extensible implementation of an Atom feed that aims to be very close to RFC4287.

20
atom.go
View File

@ -1,10 +1,12 @@
package atom package atom
import ( import (
"fmt"
"mime" "mime"
"regexp" "regexp"
"strings" "strings"
"github.com/google/uuid"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
@ -61,8 +63,17 @@ func isXMLMediaType(mediaType string) bool {
// isValidMediaType checks whether a string is a valid media type. It returns a // isValidMediaType checks whether a string is a valid media type. It returns a
// bool. // bool.
func isValidMediaType(mediaType string) bool { func isValidMediaType(mediaType string) bool {
_, _, err := mime.ParseMediaType(mediaType) mediaType, _, err := mime.ParseMediaType(mediaType)
return err == nil if err != nil {
return false
}
typeParts := strings.Split(mediaType, "/")
if len(typeParts) != 2 || typeParts[0] == "" || typeParts[1] == "" {
return false
}
return true
} }
// isValidLanguageTag checks whether a LanguageTag is valid. It returns a bool. // isValidLanguageTag checks whether a LanguageTag is valid. It returns a bool.
@ -70,3 +81,8 @@ func isValidLanguageTag(tag LanguageTag) bool {
_, err := language.Parse(string(tag)) _, err := language.Parse(string(tag))
return err == nil return err == nil
} }
// NewURN generates an new valid IRI based on a UUID. It returns an IRI.
func NewURN() IRI {
return IRI(fmt.Sprint("urn:uuid:", uuid.New()))
}

View File

@ -1,27 +1,23 @@
package atom package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"html" "html"
) )
type Category struct { type Category struct {
XMLName xml.Name `xml:"category"`
*CommonAttributes *CommonAttributes
Content Content `xml:"content"` // undefinedContent in RFC4287 Term string `xml:"term,attr"`
Term string `xml:"term,attr"` Scheme IRI `xml:"scheme,attr,omitempty"`
Scheme IRI `xml:"scheme,attr,omitempty"` Label string `xml:"label,attr,omitempty"`
Label string `xml:"label,attr,omitempty"`
} }
// NewCategory creates a new Category. It returns a *Category and an error. // NewCategory creates a new Category. It returns a *Category.
func NewCategory(term string) (*Category, error) { func NewCategory(term string) *Category {
content, err := NewContent(InlineText, "", "") return &Category{Term: term}
if err != nil {
return nil, fmt.Errorf("error creating content element: %v", err)
}
return &Category{Term: term, Content: content}, nil
} }
// SetLabel sets the label of the Category. // SetLabel sets the label of the Category.
@ -46,13 +42,5 @@ func (c *Category) Check() error {
return fmt.Errorf("label attribute %v of category not correctly escaped", c.Label) return fmt.Errorf("label attribute %v of category not correctly escaped", c.Label)
} }
if c.Content == nil {
return errors.New("no content element of category")
} else {
if err := c.Content.Check(); err != nil {
return fmt.Errorf("content element of category: %v", err)
}
}
return nil return nil
} }

View File

@ -8,6 +8,22 @@ type CommonAttributes struct {
UndefinedAttributes []*ExtensionAttribute `xml:",any"` UndefinedAttributes []*ExtensionAttribute `xml:",any"`
} }
// NewCommonAttributes creates a new set of CommonAttributes. It returns a
// *CommonAttributes.
func NewCommonAttributes() *CommonAttributes {
return new(CommonAttributes)
}
// AddExtensionAttribute adds the ExtensionAttribute to the CommonAttributes.
func (c *CommonAttributes) AddExtensionAttribute(e *ExtensionAttribute) {
if c.UndefinedAttributes == nil {
c.UndefinedAttributes = make([]*ExtensionAttribute, 1)
c.UndefinedAttributes[0] = e
} else {
c.UndefinedAttributes = append(c.UndefinedAttributes, e)
}
}
// Check checks the CommonAttributes for incompatibilities with RFC4287. It // Check checks the CommonAttributes for incompatibilities with RFC4287. It
// returns an error. // returns an error.
func (c *CommonAttributes) Check() error { func (c *CommonAttributes) Check() error {

View File

@ -7,7 +7,7 @@ import (
type Date struct { type Date struct {
*CommonAttributes *CommonAttributes
DateTime string DateTime string `xml:",chardata"`
} }
// DateTime formats a time.Time to string formated as defined by RFC3339. It // DateTime formats a time.Time to string formated as defined by RFC3339. It

View File

@ -1,6 +1,7 @@
package atom package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@ -12,16 +13,17 @@ import (
// a non-empty atom:summary element when the entry contains no atom:content // a non-empty atom:summary element when the entry contains no atom:content
// element. // element.
type Entry struct { type Entry struct {
XMLName xml.Name `xml:"entry"`
*CommonAttributes *CommonAttributes
Authors []*Person `xml:"author,omitempty"` Authors []*Person `xml:"author,omitempty"`
Categories []*Category `xml:"category,omitempty"` Categories []*Category `xml:",omitempty"`
Content Content `xml:"content,omitempty"` Content Content `xml:",omitempty"`
Contributors []*Person `xml:"contributors,omitempty"` Contributors []*Person `xml:"contributors,omitempty"`
ID *ID `xml:"id"` ID *ID
Links []*Link `xml:"link,omitempty"` Links []*Link `xml:",omitempty"`
Published *Date `xml:"published,omitempty"` Published *Date `xml:"published,omitempty"`
Rights Text `xml:"rights,omitempty"` Rights Text `xml:"rights,omitempty"`
Source *Source `xml:"source,omitempty"` Source *Source `xml:",omitempty"`
Summary Text `xml:"summary,omitempty"` Summary Text `xml:"summary,omitempty"`
Title Text `xml:"title"` Title Text `xml:"title"`
Updated *Date `xml:"updated"` Updated *Date `xml:"updated"`
@ -34,10 +36,15 @@ type Entry struct {
// the atom:entry contains an atom:source element that contains an atom:author // the atom:entry contains an atom:source element that contains an atom:author
// element or, in an Atom Feed Document, the atom:feed element contains an // element or, in an Atom Feed Document, the atom:feed element contains an
// atom:author element itself. // atom:author element itself.
func (e *Entry) checkAuthors() error { func (e *Entry) checkAuthors(authorInFeed bool) error {
if e.Authors == nil { if e.Authors == nil {
if e.Source.Authors == nil { if !authorInFeed {
return errors.New("no authors set in entry") if e.Source == nil {
return errors.New("no authors set in entry")
}
if e.Source.Authors == nil {
return errors.New("no authors set in entry")
}
} }
} else { } else {
for i, a := range e.Authors { for i, a := range e.Authors {
@ -57,13 +64,66 @@ func NewEntry(title string) (*Entry, error) {
return nil, fmt.Errorf("error creating new entry: %v", err) 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)
}
return &Entry{ return &Entry{
ID: NewID(), ID: id,
Title: text, Title: text,
Updated: NewDate(time.Now()), Updated: NewDate(time.Now()),
}, nil }, nil
} }
// AddAuthor adds the Person as an author to the Entry.
func (e *Entry) AddAuthor(p *Person) {
if e.Authors == nil {
e.Authors = make([]*Person, 1)
e.Authors[0] = p
} else {
e.Authors = append(e.Authors, p)
}
e.Updated.DateTime = DateTime(time.Now())
}
// AddCategory adds the Category to the Entry.
func (e *Entry) AddCategory(c *Category) {
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())
}
// AddContributor adds the Person as a contributor to the Entry.
func (e *Entry) AddContributor(c *Person) {
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())
}
// AddLink adds the Link to the Entry.
func (e *Entry) AddLink(l *Link) {
if e.Links == nil {
e.Links = make([]*Link, 1)
e.Links[0] = l
} else {
e.Links = append(e.Links, l)
}
e.Updated.DateTime = DateTime(time.Now())
}
// AddExtension adds the ExtensionElement to the Entry. // AddExtension adds the ExtensionElement to the Entry.
func (e *Entry) AddExtension(x *ExtensionElement) { func (e *Entry) AddExtension(x *ExtensionElement) {
if e.Extensions == nil { if e.Extensions == nil {
@ -87,7 +147,7 @@ func (e *Entry) Check() error {
} }
} }
if err := e.checkAuthors(); err != nil { if err := e.checkAuthors(true); err != nil {
return fmt.Errorf("entry %v: %v", e.ID.URI, err) return fmt.Errorf("entry %v: %v", e.ID.URI, err)
} }
@ -160,7 +220,7 @@ func (e *Entry) Check() error {
// is not an XML media type [RFC3023], does not begin with "text/", and // is not an XML media type [RFC3023], does not begin with "text/", and
// does not end with "/xml" or "+xml". // does not end with "/xml" or "+xml".
mediaType := e.Content.getType() mediaType := e.Content.getType()
if !isXMLMediaType(mediaType) && !strings.HasPrefix(mediaType, "text/") { if isValidMediaType(mediaType) && !isXMLMediaType(mediaType) && !strings.HasPrefix(mediaType, "text/") {
return fmt.Errorf("no summary element of entry %v but media type not xml", e.ID.URI) return fmt.Errorf("no summary element of entry %v but media type not xml", e.ID.URI)
} }
} }

View File

@ -1,19 +1,24 @@
package atom package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt"
) )
type ExtensionAttribute struct { type ExtensionAttribute struct {
Value any `xml:",attr"` Attr string `xml:",attr"`
XMLName xml.Name }
// NewExtensionAttribute creates a new ExtensionAttribute. It returns a
// *ExtensionAttribute.
func NewExtensionAttribute(name, value string) *ExtensionAttribute {
return &ExtensionAttribute{Attr: fmt.Sprint(name, `="`, value, `"`)}
} }
// Check checks the ExtensionAttribute for incompatibilities with RFC4287. It // Check checks the ExtensionAttribute for incompatibilities with RFC4287. It
// returns an error. // returns an error.
func (e *ExtensionAttribute) Check() error { func (e *ExtensionAttribute) Check() error {
if e.Value == nil { if e.Attr == "" {
return errors.New("value element of extension attribute empty") return errors.New("value element of extension attribute empty")
} }

27
feed.go
View File

@ -10,20 +10,20 @@ import (
type Feed struct { type Feed struct {
XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"` XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"`
*CommonAttributes *CommonAttributes
Authors []*Person `xml:"author,omitempty"` Authors []*Person `xml:"author,omitempty"`
Categories []*Category `xml:"category,omitempty"` Categories []*Category `xml:",omitempty"`
Contributors []*Person `xml:"contributor,omitempty"` Contributors []*Person `xml:"contributor,omitempty"`
Generator *Generator `xml:"generator,omitempty"` Generator *Generator `xml:",omitempty"`
Icon *Icon `xml:"icon,omitempty"` Icon *Icon `xml:",omitempty"`
ID *ID `xml:"id"` ID *ID
Links []*Link `xml:"link,omitempty"` Links []*Link `xml:",omitempty"`
Logo *Logo `xml:"logo,omitempty"` Logo *Logo `xml:",omitempty"`
Rights Text `xml:"rights,omitempty"` Rights Text `xml:"rights,omitempty"`
Subtitle Text `xml:"subtitle,omitempty"` Subtitle Text `xml:"subtitle,omitempty"`
Title Text `xml:"title"` Title Text `xml:"title"`
Updated *Date `xml:"updated"` Updated *Date `xml:"updated"`
Extensions []*ExtensionElement `xml:",any,omitempty"` Extensions []*ExtensionElement `xml:",any,omitempty"`
Entries []*Entry `xml:"entry,omitempty"` Entries []*Entry `xml:",omitempty"`
} }
// NewFeed creates a new Feed. It returns a *Feed and an error. // NewFeed creates a new Feed. It returns a *Feed and an error.
@ -33,8 +33,13 @@ func NewFeed(title string) (*Feed, error) {
return nil, fmt.Errorf("error creating new feed: %v", err) 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)
}
return &Feed{ return &Feed{
ID: NewID(), ID: id,
Title: text, Title: text,
Updated: NewDate(time.Now()), Updated: NewDate(time.Now()),
}, nil }, nil
@ -128,7 +133,7 @@ func (f *Feed) Check() error {
// least one atom:author element. // least one atom:author element.
if f.Authors == nil { if f.Authors == nil {
for _, e := range f.Entries { for _, e := range f.Entries {
if err := e.checkAuthors(); err != nil { if err := e.checkAuthors(false); err != nil {
return fmt.Errorf("no authors set in feed %v: %v", f.ID.URI, err) return fmt.Errorf("no authors set in feed %v: %v", f.ID.URI, err)
} }
} }

View File

@ -1,16 +1,18 @@
package atom package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"html" "html"
) )
type Generator struct { type Generator struct {
XMLName xml.Name `xml:"generator"`
*CommonAttributes *CommonAttributes
URI IRI `xml:"uri,attr,omitempty"` URI IRI `xml:"uri,attr,omitempty"`
Version string `xml:"version,attr,omitempty"` Version string `xml:"version,attr,omitempty"`
Text string `xml:"text"` Text string `xml:",chardata"`
} }
// NewGenerator creates a new Generator. It returns a *Generator. // NewGenerator creates a new Generator. It returns a *Generator.

14
icon.go
View File

@ -1,18 +1,24 @@
package atom package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
) )
type Icon struct { type Icon struct {
XMLName xml.Name `xml:"icon"`
*CommonAttributes *CommonAttributes
URI IRI `xml:"uri"` URI IRI `xml:",chardata"`
} }
// NewIcon creates a new Icon. It returns a *Icon. // NewIcon creates a new Icon. It returns a *Icon and an error.
func NewIcon(uri string) *Icon { func NewIcon(uri IRI) (*Icon, error) {
return &Icon{URI: IRI(uri)} if !isValidIRI(uri) {
return nil, fmt.Errorf("uri %v not correctly formatted", uri)
}
return &Icon{URI: uri}, nil
} }
// Check checks the Icon for incompatibilities with RFC4287. It returns an // Check checks the Icon for incompatibilities with RFC4287. It returns an

16
id.go
View File

@ -1,20 +1,24 @@
package atom package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"github.com/google/uuid"
) )
type ID struct { type ID struct {
XMLName xml.Name `xml:"id"`
*CommonAttributes *CommonAttributes
URI IRI `xml:"uri"` URI IRI `xml:",chardata"`
} }
// NewID creates a new ID. It returns a *ID. // NewID creates a new ID. It returns a *ID and an error.
func NewID() *ID { func NewID(uri IRI) (*ID, error) {
return &ID{URI: IRI(fmt.Sprint("urn:uuid:", uuid.New()))} if !isValidIRI(uri) {
return nil, fmt.Errorf("uri %v not correctly formatted", uri)
}
return &ID{URI: IRI(uri)}, nil
} }
// Check checks the ID for incompatibilities with RFC4287. It returns an error. // Check checks the ID for incompatibilities with RFC4287. It returns an error.

View File

@ -1,14 +1,16 @@
package atom package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"mime" "mime"
) )
type InlineOtherContent struct { type InlineOtherContent struct {
XMLName xml.Name `xml:"content"`
*CommonAttributes *CommonAttributes
AnyElement any `xml:"anyelement,omitempty"` AnyElement any `xml:",chardata"`
Type MediaType `xml:"type,attr,omitempty"` Type MediaType `xml:"type,attr,omitempty"`
} }

View File

@ -1,15 +1,16 @@
package atom package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"reflect"
) )
type InlineTextContent struct { type InlineTextContent struct {
XMLName xml.Name `xml:"content"`
*CommonAttributes *CommonAttributes
Type string `xml:"type,attr,omitempty"` // Must be text or html Type string `xml:"type,attr,omitempty"` // Must be text or html
Texts []string `xml:"texts,omitempty"` Text string `xml:",chardata"`
} }
// newInlineTextContent creates a new InlineTextContent. It returns a // newInlineTextContent creates a new InlineTextContent. It returns a
@ -19,23 +20,12 @@ func newInlineTextContent(mediaType string, content any) (*InlineTextContent, er
return nil, fmt.Errorf("media type %v incompatible with inline text content", mediaType) return nil, fmt.Errorf("media type %v incompatible with inline text content", mediaType)
} }
texts := make([]string, 0) text, ok := content.(string)
if !ok {
t := reflect.TypeOf(content)
switch t.Kind() {
case reflect.Slice:
if t.Elem().Kind() == reflect.String {
for _, t := range content.([]string) {
texts = append(texts, t)
}
}
case reflect.String:
texts = append(texts, content.(string))
default:
return nil, fmt.Errorf("content type %T incompatible with inline text content", content) return nil, fmt.Errorf("content type %T incompatible with inline text content", content)
} }
return &InlineTextContent{Type: mediaType, Texts: texts}, nil return &InlineTextContent{Type: mediaType, Text: text}, nil
} }
// isContent checks whether the InlineTextContent is a Content. It returns a // isContent checks whether the InlineTextContent is a Content. It returns a

View File

@ -1,15 +1,16 @@
package atom package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"reflect"
) )
type InlineXHTMLContent struct { type InlineXHTMLContent struct {
XMLName xml.Name `xml:"content"`
*CommonAttributes *CommonAttributes
XHTMLDiv *XHTMLDiv
Type string `xml:"type,attr"` Type string `xml:"type,attr"`
XHTMLDiv string `xml:"xhtmldiv"`
} }
// newInlineXHTMLContent creates a new InlineXHTMLContent. It returns a // newInlineXHTMLContent creates a new InlineXHTMLContent. It returns a
@ -19,11 +20,12 @@ func newInlineXHTMLContent(mediaType string, content any) (*InlineXHTMLContent,
return nil, fmt.Errorf("media type %v incompatible with inline xhtml content", mediaType) return nil, fmt.Errorf("media type %v incompatible with inline xhtml content", mediaType)
} }
if reflect.TypeOf(content).Kind() != reflect.String { xhtmlDiv, ok := content.(*XHTMLDiv)
if !ok {
return nil, fmt.Errorf("content type %T incompatible with inline xhtml content", content) return nil, fmt.Errorf("content type %T incompatible with inline xhtml content", content)
} }
return &InlineXHTMLContent{Type: mediaType, XHTMLDiv: content.(string)}, nil return &InlineXHTMLContent{Type: mediaType, XHTMLDiv: xhtmlDiv}, nil
} }
// isContent checks whether the InlineXHTMLContent is a Content. It returns a // isContent checks whether the InlineXHTMLContent is a Content. It returns a
@ -44,8 +46,8 @@ func (i *InlineXHTMLContent) Check() error {
return errors.New("type attribute of inline xhtml content must be xhtml") return errors.New("type attribute of inline xhtml content must be xhtml")
} }
if i.XHTMLDiv == "" { if err := i.XHTMLDiv.Check(); err != nil {
return errors.New("xhtmlDiv element of inline xhtml content empty") return fmt.Errorf("xhtml div element %v of inline xhtml content %v: %v", i.XHTMLDiv, i, err)
} }
return nil return nil

38
link.go
View File

@ -1,15 +1,16 @@
package atom package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
) )
type Link struct { type Link struct {
XMLName xml.Name `xml:"link"`
*CommonAttributes *CommonAttributes
Title string `xml:"title,attr,omitempty"` Title string `xml:"title,attr,omitempty"`
Content Content `xml:"content"` // undefinedContent in RFC4287
Href IRI `xml:"href,attr"` Href IRI `xml:"href,attr"`
Rel string `xml:"rel,attr,omitempty"` Rel string `xml:"rel,attr,omitempty"`
Type MediaType `xml:"type,attr,omitempty"` Type MediaType `xml:"type,attr,omitempty"`
@ -17,14 +18,9 @@ type Link struct {
Length uint `xml:"length,attr,omitempty"` Length uint `xml:"length,attr,omitempty"`
} }
// NewLink creates a new Link. It returns a *Link and an error. // NewLink creates a new Link. It returns a *Link.
func NewLink(href string) (*Link, error) { func NewLink(href string) *Link {
content, err := NewContent(InlineText, "", "") return &Link{Href: IRI(href)}
if err != nil {
return nil, fmt.Errorf("error creating content element: %v", err)
}
return &Link{Href: IRI(href), Content: content}, nil
} }
// Check checks the Link for incompatibilities with RFC4287. It returns an // Check checks the Link for incompatibilities with RFC4287. It returns an
@ -38,23 +34,21 @@ func (l *Link) Check() error {
} }
} }
if strings.Contains(l.Rel, ":") || !isValidIRI(IRI(l.Rel)) { if l.Rel != "" {
return fmt.Errorf("rel attribute %v of link %v not correctly formatted", l.Rel, l.Href) if strings.Contains(l.Rel, ":") && !isValidIRI(IRI(l.Rel)) {
return fmt.Errorf("rel attribute %v of link %v not correctly formatted", l.Rel, l.Href)
}
} }
if !isValidMediaType(string(l.Type)) { if l.Type != "" {
return fmt.Errorf("type attribute %v of link %v invalid media type", l.Type, l.Href) if !isValidMediaType(string(l.Type)) {
return fmt.Errorf("type attribute %v of link %v invalid media type", l.Type, l.Href)
}
} }
if !isValidLanguageTag(l.HrefLang) { if l.HrefLang != "" {
return fmt.Errorf("hreflang attribute %v of link %v invalid language tag", l.Type, l.HrefLang) if !isValidLanguageTag(l.HrefLang) {
} return fmt.Errorf("hreflang attribute %v of link %v invalid language tag", l.Type, l.HrefLang)
if l.Content == nil {
return fmt.Errorf("no content element of link %v", l.Href)
} else {
if err := l.Content.Check(); err != nil {
return fmt.Errorf("content element of link %v: %v", l.Href, err)
} }
} }

14
logo.go
View File

@ -1,20 +1,24 @@
package atom package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"github.com/google/uuid"
) )
type Logo struct { type Logo struct {
XMLName xml.Name `xml:"logo"`
*CommonAttributes *CommonAttributes
URI IRI `xml:"uri"` URI IRI `xml:",chardata"`
} }
// NewLogo creates a new Logo. It returns a *Logo. // NewLogo creates a new Logo. It returns a *Logo.
func NewLogo() *Logo { func NewLogo(uri IRI) (*Logo, error) {
return &Logo{URI: IRI(fmt.Sprint("urn:uuid:", uuid.New()))} if !isValidIRI(uri) {
return nil, fmt.Errorf("uri %v not correctly formatted", uri)
}
return &Logo{URI: uri}, nil
} }
// Check checks the Logo for incompatibilities with RFC4287. It returns an // Check checks the Logo for incompatibilities with RFC4287. It returns an

View File

@ -1,13 +1,14 @@
package atom package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"mime" "mime"
"reflect"
) )
type OutOfLineContent struct { type OutOfLineContent struct {
XMLName xml.Name `xml:"content"`
*CommonAttributes *CommonAttributes
Type MediaType `xml:"type,attr,omitempty"` Type MediaType `xml:"type,attr,omitempty"`
SRC IRI `xml:"src,attr"` SRC IRI `xml:"src,attr"`
@ -20,15 +21,16 @@ func newOutOfLineContent(mediaType string, content any) (*OutOfLineContent, erro
return nil, fmt.Errorf("media type %v incompatible with out of line content", mediaType) return nil, fmt.Errorf("media type %v incompatible with out of line content", mediaType)
} }
if reflect.TypeOf(content).Kind() != reflect.String { iri, ok := content.(IRI)
if !ok {
return nil, fmt.Errorf("content type %T incompatible with out of line content", content) return nil, fmt.Errorf("content type %T incompatible with out of line content", content)
} }
if !isValidIRI(content.(IRI)) { if !isValidIRI(iri) {
return nil, errors.New("content not a valid uri") return nil, errors.New("content not a valid uri")
} }
return &OutOfLineContent{Type: MediaType(mediaType), SRC: content.(IRI)}, nil return &OutOfLineContent{Type: MediaType(mediaType), SRC: iri}, nil
} }
// isContent checks whether the OutOfLineContent is a Content. It returns a // isContent checks whether the OutOfLineContent is a Content. It returns a

View File

@ -5,7 +5,7 @@ import "errors"
type PlainText struct { type PlainText struct {
*CommonAttributes *CommonAttributes
Type string `xml:"type,attr,omitempty"` // Must be text or html Type string `xml:"type,attr,omitempty"` // Must be text or html
Text string `xml:"text"` Text string `xml:",chardata"`
} }
// isText checks whether the PlainText is a Text. It returns a bool. // isText checks whether the PlainText is a Text. It returns a bool.

View File

@ -1,17 +1,21 @@
package atom package atom
import "fmt" import (
"encoding/xml"
"fmt"
)
type Source struct { type Source struct {
XMLName xml.Name `xml:"source"`
*CommonAttributes *CommonAttributes
Authors []*Person `xml:"author,omitempty"` Authors []*Person `xml:"author,omitempty"`
Categories []*Category `xml:"category,omitempty"` Categories []*Category `xml:",omitempty"`
Contributors []*Person `xml:"contributor,omitempty"` Contributors []*Person `xml:"contributor,omitempty"`
Generator *Generator `xml:"generator,omitempty"` Generator *Generator `xml:",omitempty"`
Icon *Icon `xml:"icon,omitempty"` Icon *Icon `xml:",omitempty"`
ID *ID `xml:"id,omitempty"` ID *ID `xml:",omitempty"`
Links []*Link `xml:"link,omitempty"` Links []*Link `xml:",omitempty"`
Logo *Logo `xml:"logo,omitempty"` Logo *Logo `xml:",omitempty"`
Rights Text `xml:"rights,omitempty"` Rights Text `xml:"rights,omitempty"`
Subtitle Text `xml:"subtitle,omitempty"` Subtitle Text `xml:"subtitle,omitempty"`
Title Text `xml:"title,omitempty"` Title Text `xml:"title,omitempty"`

View File

@ -20,7 +20,7 @@ func NewText(textType, content string) (Text, error) {
case "xhtml": case "xhtml":
return &XHTMLText{ return &XHTMLText{
Type: textType, Type: textType,
XHTMLDiv: XHTMLDiv{ XHTMLDiv: &XHTMLDiv{
XMLNS: "http://www.w3.org/1999/xhtml", XMLNS: "http://www.w3.org/1999/xhtml",
Content: content, Content: content,
}, },

30
xhtmlDiv.go Normal file
View File

@ -0,0 +1,30 @@
package atom
import (
"encoding/xml"
"errors"
)
type XHTMLDiv struct {
XMLName xml.Name `xml:"div"`
XMLNS string `xml:"xmlns,attr"`
Content string `xml:",innerxml"`
}
// 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,
}
}
// Check checks the XHTMLDiv for incompatibilities with RFC4287. It returns an
// error.
func (x *XHTMLDiv) Check() error {
if x.XMLNS != "http://www.w3.org/1999/xhtml" {
return errors.New("xmlns attribute of xhtml text must be http://www.w3.org/1999/xhtml")
}
return nil
}

View File

@ -1,20 +1,14 @@
package atom package atom
import ( import (
"encoding/xml"
"errors" "errors"
"fmt"
) )
type XHTMLDiv struct {
XMLName xml.Name `xml:"div"`
XMLNS string `xml:"xmlns,attr"`
Content string `xml:",innerxml"`
}
type XHTMLText struct { type XHTMLText struct {
*CommonAttributes *CommonAttributes
XHTMLDiv *XHTMLDiv
Type string `xml:"type,attr"` // Must be xhtml Type string `xml:"type,attr"` // Must be xhtml
XHTMLDiv XHTMLDiv
} }
// isText checks whether the XHTMLText is a Text. It returns a bool. // isText checks whether the XHTMLText is a Text. It returns a bool.
@ -27,8 +21,8 @@ func (x *XHTMLText) Check() error {
return errors.New("type attribute of xhtml text must be xhtml") return errors.New("type attribute of xhtml text must be xhtml")
} }
if x.XHTMLDiv.XMLNS != "http://www.w3.org/1999/xhtml" { if err := x.XHTMLDiv.Check(); err != nil {
return errors.New("xmlns attribute of xhtml text must be http://www.w3.org/1999/xhtml") return fmt.Errorf("xhtml div element %v of xhtml text %v: %v", x.XHTMLDiv, x, err)
} }
return nil return nil