From 92c794d070d412e983b922e1b362b41c05d7a595 Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Sun, 13 Oct 2024 17:19:40 +0200 Subject: [PATCH] First implementation of an atom feed --- atom.go | 8 ++ category.go | 30 ++++++++ commonAttributes.go | 19 +++++ content.go | 6 ++ date.go | 23 ++++++ entry.go | 138 +++++++++++++++++++++++++++++++++ extensionAttribute.go | 19 +++++ extensionElement.go | 19 +++++ feed.go | 174 ++++++++++++++++++++++++++++++++++++++++++ generator.go | 18 +++++ icon.go | 16 ++++ id.go | 16 ++++ inlineOtherContent.go | 11 +++ inlineTextContent.go | 19 +++++ inlineXHTMLContent.go | 23 ++++++ link.go | 39 ++++++++++ logo.go | 16 ++++ outOfLineContent.go | 19 +++++ person.go | 28 +++++++ plainText.go | 23 ++++++ source.go | 112 +++++++++++++++++++++++++++ text.go | 6 ++ xhtmlText.go | 23 ++++++ 23 files changed, 805 insertions(+) create mode 100644 atom.go create mode 100644 category.go create mode 100644 commonAttributes.go create mode 100644 content.go create mode 100644 date.go create mode 100644 entry.go create mode 100644 extensionAttribute.go create mode 100644 extensionElement.go create mode 100644 feed.go create mode 100644 generator.go create mode 100644 icon.go create mode 100644 id.go create mode 100644 inlineOtherContent.go create mode 100644 inlineTextContent.go create mode 100644 inlineXHTMLContent.go create mode 100644 link.go create mode 100644 logo.go create mode 100644 outOfLineContent.go create mode 100644 person.go create mode 100644 plainText.go create mode 100644 source.go create mode 100644 text.go create mode 100644 xhtmlText.go diff --git a/atom.go b/atom.go new file mode 100644 index 0000000..82715a4 --- /dev/null +++ b/atom.go @@ -0,0 +1,8 @@ +package atomfeed + +type ( + EmailAddress string + LanguageTag string + MediaType string + URI string +) diff --git a/category.go b/category.go new file mode 100644 index 0000000..00ed249 --- /dev/null +++ b/category.go @@ -0,0 +1,30 @@ +package atomfeed + +import ( + "errors" + "fmt" +) + +type Category struct { + *CommonAttributes + Content *Content `xml:"content"` + Term string `xml:"term,attr"` + Scheme URI `xml:"scheme,attr,omitempty"` + Label string `xml:"label,attr,omitempty"` +} + +func (c *Category) Check() error { + if c.Term == "" { + return errors.New("term attribute of category empty") + } + + 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 +} diff --git a/commonAttributes.go b/commonAttributes.go new file mode 100644 index 0000000..1e4737b --- /dev/null +++ b/commonAttributes.go @@ -0,0 +1,19 @@ +package atomfeed + +import "fmt" + +type CommonAttributes struct { + Base URI `xml:"base,attr,omitempty"` + Lang LanguageTag `xml:"lang,attr,omitempty"` + UndefinedAttributes []*ExtensionAttribute `xml:",any"` +} + +func (c *CommonAttributes) Check() error { + for i, e := range c.UndefinedAttributes { + if err := e.Check(); err != nil { + return fmt.Errorf("extension attribute %v of common attributes: %v", i, err) + } + } + + return nil +} diff --git a/content.go b/content.go new file mode 100644 index 0000000..5f95d6f --- /dev/null +++ b/content.go @@ -0,0 +1,6 @@ +package atomfeed + +type Content interface { + Check() error + IsContent() bool +} diff --git a/date.go b/date.go new file mode 100644 index 0000000..486a754 --- /dev/null +++ b/date.go @@ -0,0 +1,23 @@ +package atomfeed + +import ( + "errors" + "time" +) + +type Date struct { + *CommonAttributes + DateTime string +} + +func (d *Date) Check() error { + if d.DateTime == "" { + return errors.New("date time element of date is empty") + } + + return nil +} + +func DateTime(t time.Time) string { + return string(t.Format(time.RFC3339)) +} diff --git a/entry.go b/entry.go new file mode 100644 index 0000000..3018e6a --- /dev/null +++ b/entry.go @@ -0,0 +1,138 @@ +package atomfeed + +import ( + "encoding/xml" + "errors" + "fmt" +) + +type Entry struct { + *CommonAttributes + Authors []*Person `xml:"author,omitempty"` + Categories []*Category `xml:"category,omitempty"` + Content *Content `xml:"content,omitempty"` + Contributors []*Person `xml:"contributors,omitempty"` + ID *ID `xml:"id"` + Links []*Link `xml:"link,omitempty"` + Published *Date `xml:"published,omitempty"` + Rights *Text `xml:"rights,omitempty"` + Source *Source `xml:"source,omitempty"` + Summary *Text `xml:"summary,omitempty"` + Title *Text `xml:"title"` + Updated *Date `xml:"updated"` + Extensions []*ExtensionElement `xml:",any"` +} + +func (e *Entry) checkAuthors() error { + if e.Authors == nil { + if e.Source.Authors == nil { + return errors.New("no authors set in entry") + } + } else { + for i, a := range e.Authors { + if err := a.Check(); err != nil { + return fmt.Errorf("author element %v of entry: %v", i, err) + } + } + } + + return nil +} + +func (e *Entry) Check() error { + if e.ID == nil { + return errors.New("no id element of entry") + } else { + if err := e.ID.Check(); err != nil { + return fmt.Errorf("id element of entry: %v", err) + } + } + + if err := e.checkAuthors(); err != nil { + return fmt.Errorf("entry %v: %v", e.ID.URI, err) + } + + if e.Categories != nil { + for i, c := range e.Categories { + if err := c.Check(); err != nil { + 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.ID.URI, err) + } + } + + if e.Contributors != nil { + for i, c := range e.Contributors { + if err := c.Check(); err != nil { + return fmt.Errorf("contributor element %v of entry %v: %v", i, e.ID.URI, err) + } + } + } + + if e.Links != nil { + for i, l := range e.Links { + if err := l.Check(); err != nil { + return fmt.Errorf("link element %v of entry %v: %v", i, e.ID.URI, err) + } + } + } + + if e.Published != nil { + if err := e.Published.Check(); err != nil { + 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.ID.URI, err) + } + } + + if e.Source != nil { + if err := e.Source.Check(); err != nil { + 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.ID.URI, err) + } + } + + if e.Title == nil { + 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.ID.URI, err) + } + } + + if e.Updated == nil { + 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.ID.URI, err) + } + } + + if e.Extensions != nil { + for i, x := range e.Extensions { + if err := x.Check(); err != nil { + return fmt.Errorf("extension element %v of entry %v: %v", i, e.ID.URI, err) + } + } + } + + return nil +} + +func (e *Entry) AddExtension(name string, value any) { + e.Extensions = append(e.Extensions, &ExtensionElement{XMLName: xml.Name{Local: name}, Value: value}) +} diff --git a/extensionAttribute.go b/extensionAttribute.go new file mode 100644 index 0000000..736ca3e --- /dev/null +++ b/extensionAttribute.go @@ -0,0 +1,19 @@ +package atomfeed + +import ( + "encoding/xml" + "errors" +) + +type ExtensionAttribute struct { + Value any `xml:",attr"` + XMLName xml.Name +} + +func (e *ExtensionAttribute) Check() error { + if e.Value == nil { + return errors.New("value element of extension attribute empty") + } + + return nil +} diff --git a/extensionElement.go b/extensionElement.go new file mode 100644 index 0000000..faeaafb --- /dev/null +++ b/extensionElement.go @@ -0,0 +1,19 @@ +package atomfeed + +import ( + "encoding/xml" + "errors" +) + +type ExtensionElement struct { + Value any `xml:",innerxml"` + XMLName xml.Name +} + +func (e *ExtensionElement) Check() error { + if e.Value == nil { + return errors.New("value element of extension element empty") + } + + return nil +} diff --git a/feed.go b/feed.go new file mode 100644 index 0000000..fe13c8c --- /dev/null +++ b/feed.go @@ -0,0 +1,174 @@ +package atomfeed + +import ( + "encoding/xml" + "errors" + "fmt" +) + +type Feed struct { + XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"` + *CommonAttributes + Authors []*Person `xml:"author,omitempty"` + Categories []*Category `xml:"category,omitempty"` + Contributors []*Person `xml:"contributor,omitempty"` + Generator *Generator `xml:"generator,omitempty"` + Icon *Icon `xml:"icon,omitempty"` + ID *ID `xml:"id"` + Links []*Link `xml:"link,omitempty"` // There should be one link with rel "self" + Logo *Logo `xml:"logo,omitempty"` + Rights *Text `xml:"rights,omitempty"` + Subtitle *Text `xml:"subtitle,omitempty"` + Title *Text `xml:"title"` + Updated *Date `xml:"updated"` + Extensions []*ExtensionElement `xml:",any"` + Entries []*Entry `xml:"entry,omitempty"` +} + +func (f *Feed) Check() error { + if f.ID == nil { + return errors.New("no id element of feed") + } else { + if err := f.ID.Check(); err != nil { + return fmt.Errorf("id element of feed: %v", err) + } + } + + if f.Authors == nil { + for _, e := range f.Entries { + if err := e.checkAuthors(); err != nil { + 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.ID.URI, err) + } + } + } + + if f.Categories != nil { + for i, c := range f.Categories { + if err := c.Check(); err != nil { + return fmt.Errorf("category element %v of feed %v: %v", i, f.ID.URI, err) + } + } + } + + if f.Contributors != nil { + for i, c := range f.Contributors { + if err := c.Check(); err != nil { + 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.ID.URI, err) + } + } + + if f.Icon != nil { + if err := f.Icon.Check(); err != nil { + return fmt.Errorf("icon element of feed %v: %v", f.ID.URI, err) + } + } + + if f.Links != nil { + for i, l := range f.Links { + if err := l.Check(); err != nil { + return fmt.Errorf("link element %v of feed %v: %v", i, f.ID.URI, err) + } + } + } + + if f.Logo != nil { + if err := f.Logo.Check(); err != nil { + 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.ID.URI, err) + } + } + + if f.Subtitle != nil { + if err := (*f.Subtitle).Check(); err != nil { + 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.ID.URI) + } else { + if err := (*f.Title).Check(); err != nil { + 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.ID) + } else { + if err := f.Updated.Check(); err != nil { + return fmt.Errorf("updated element of feed %v: %v", f.ID.URI, err) + } + } + + if f.Extensions != nil { + for i, x := range f.Extensions { + if err := x.Check(); err != nil { + return fmt.Errorf("extension element %v of feed %v: %v", i, f.ID.URI, err) + } + } + } + + if f.Entries != nil { + for i, n := range f.Entries { + if err := n.Check(); err != nil { + return fmt.Errorf("entry element %v of feed %v: %v", i, f.ID.URI, err) + } + } + } + + return nil +} + +// TODO: Create complete link or delete +func (f *Feed) Standardize() { + if f.Links == nil { + f.Links = make([]*Link, 1) + f.Links[0] = &Link{Rel: "self"} + } else { + selfExists := false + for _, l := range f.Links { + if l.Rel == "self" { + selfExists = true + break + } + } + if !selfExists { + f.Links = append(f.Links, &Link{Rel: "self"}) + } + } +} + +func (f *Feed) AddExtension(name string, value any) { + f.Extensions = append(f.Extensions, &ExtensionElement{XMLName: xml.Name{Local: name}, Value: value}) +} + +func (f *Feed) ToXML(encoding string) (string, error) { + if err := f.Check(); err != nil { + return "", fmt.Errorf("error checking feed: %v", err) + } + + xml, err := xml.MarshalIndent(f, "", " ") + if err != nil { + return "", fmt.Errorf("error xml encoding feed: %v", err) + } + + return fmt.Sprintln(``) + string(xml), nil +} diff --git a/generator.go b/generator.go new file mode 100644 index 0000000..4a67122 --- /dev/null +++ b/generator.go @@ -0,0 +1,18 @@ +package atomfeed + +import "errors" + +type Generator struct { + *CommonAttributes + URI URI `xml:"uri,attr,omitempty"` + Version string `xml:"version,attr,omitempty"` + Text string `xml:"text"` +} + +func (g *Generator) Check() error { + if g.Text == "" { + return errors.New("text element of generator empty") + } + + return nil +} diff --git a/icon.go b/icon.go new file mode 100644 index 0000000..aac2b88 --- /dev/null +++ b/icon.go @@ -0,0 +1,16 @@ +package atomfeed + +import "errors" + +type Icon struct { + *CommonAttributes + URI URI `xml:"uri"` +} + +func (i *Icon) Check() error { + if i.URI == "" { + return errors.New("uri element of icon empty") + } + + return nil +} diff --git a/id.go b/id.go new file mode 100644 index 0000000..de938f0 --- /dev/null +++ b/id.go @@ -0,0 +1,16 @@ +package atomfeed + +import "errors" + +type ID struct { + *CommonAttributes + URI URI `xml:"uri"` +} + +func (i *ID) Check() error { + if i.URI == "" { + return errors.New("uri element of id empty") + } + + return nil +} diff --git a/inlineOtherContent.go b/inlineOtherContent.go new file mode 100644 index 0000000..a2b0973 --- /dev/null +++ b/inlineOtherContent.go @@ -0,0 +1,11 @@ +package atomfeed + +type InlineOtherContent struct { + *CommonAttributes + Type MediaType `xml:"type,attr,omitempty"` + AnyElement []*any `xml:"anyelement,omitempty"` +} + +func (i *InlineOtherContent) IsContent() bool { return true } + +func (i *InlineOtherContent) Check() error { return nil } diff --git a/inlineTextContent.go b/inlineTextContent.go new file mode 100644 index 0000000..7a3fbfb --- /dev/null +++ b/inlineTextContent.go @@ -0,0 +1,19 @@ +package atomfeed + +import "errors" + +type InlineTextContent struct { + *CommonAttributes + Type string `xml:"type,attr,omitempty"` // Must be text or html + Texts []string `xml:"texts,omitempty"` +} + +func (i *InlineTextContent) IsContent() bool { return true } + +func (i *InlineTextContent) Check() error { + if i.Type != "" && i.Type != "text" && i.Type != "html" { + return errors.New("type attribute of inline text content must be text or html if not omitted") + } + + return nil +} diff --git a/inlineXHTMLContent.go b/inlineXHTMLContent.go new file mode 100644 index 0000000..da33782 --- /dev/null +++ b/inlineXHTMLContent.go @@ -0,0 +1,23 @@ +package atomfeed + +import "errors" + +type InlineXHTMLContent struct { + *CommonAttributes + Type string `xml:"type,attr"` + XHTMLDiv string `xml:"xhtmldiv"` +} + +func (i *InlineXHTMLContent) IsContent() bool { return true } + +func (i *InlineXHTMLContent) Check() error { + if i.Type != "xhtml" { + return errors.New("type attribute of inline xhtml content must be xhtml") + } + + if i.XHTMLDiv == "" { + return errors.New("xhtmlDiv element of inline xhtml content empty") + } + + return nil +} diff --git a/link.go b/link.go new file mode 100644 index 0000000..cdfe4c7 --- /dev/null +++ b/link.go @@ -0,0 +1,39 @@ +package atomfeed + +import ( + "errors" + "fmt" +) + +type Link struct { + *CommonAttributes + Title *Text `xml:"title,attr,omitempty"` + Content *Content `xml:"content"` + Href URI `xml:"href,attr"` + Rel string `xml:"rel,attr,omitempty"` + Type MediaType `xml:"type,attr,omitempty"` + HrefLang LanguageTag `xml:"hreflang,attr,omitempty"` + Length uint `xml:"length,attr,omitempty"` +} + +func (l *Link) Check() error { + if l.Href == "" { + return errors.New("href attribute of link empty") + } + + if l.Title != nil { + if err := (*l.Title).Check(); err != nil { + return fmt.Errorf("title attribute of link %v: %v", l.Href, err) + } + } + + 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) + } + } + + return nil +} diff --git a/logo.go b/logo.go new file mode 100644 index 0000000..d794d45 --- /dev/null +++ b/logo.go @@ -0,0 +1,16 @@ +package atomfeed + +import "errors" + +type Logo struct { + *CommonAttributes + URI URI `xml:"uri"` +} + +func (l *Logo) Check() error { + if l.URI == "" { + return errors.New("uri element of logo empty") + } + + return nil +} diff --git a/outOfLineContent.go b/outOfLineContent.go new file mode 100644 index 0000000..72fd200 --- /dev/null +++ b/outOfLineContent.go @@ -0,0 +1,19 @@ +package atomfeed + +import "errors" + +type OutOfLineContent struct { + *CommonAttributes + Type MediaType `xml:"type,attr,omitempty"` + SRC URI `xml:"src,attr"` +} + +func (o *OutOfLineContent) IsContent() bool { return true } + +func (o *OutOfLineContent) Check() error { + if o.SRC == "" { + return errors.New("src attribute of out of line content empty") + } + + return nil +} diff --git a/person.go b/person.go new file mode 100644 index 0000000..a22b18f --- /dev/null +++ b/person.go @@ -0,0 +1,28 @@ +package atomfeed + +import ( + "errors" + "fmt" +) + +type Person struct { + *CommonAttributes + Name string `xml:"name"` + URI URI `xml:"uri,omitempty"` + Email EmailAddress `xml:"email,omitempty"` + Extensions []*ExtensionElement `xml:",any"` +} + +func (p *Person) Check() error { + if p.Name == "" { + return errors.New("name element of person element empty") + } + + for i, e := range p.Extensions { + if err := e.Check(); err != nil { + return fmt.Errorf("extension element %v of person %v: %v", i, p.Name, err) + } + } + + return nil +} diff --git a/plainText.go b/plainText.go new file mode 100644 index 0000000..6d4eb3d --- /dev/null +++ b/plainText.go @@ -0,0 +1,23 @@ +package atomfeed + +import "errors" + +type PlainText struct { + *CommonAttributes + Type string `xml:"type,attr,omitempty"` // Must be text or html + Text string `xml:"text"` +} + +func (p *PlainText) IsText() bool { return true } + +func (p *PlainText) Check() error { + if p.Type != "" && p.Type != "text" && p.Type != "html" { + return errors.New("type attribute of plain text must be text or html if not omitted") + } + + if p.Text == "" { + return errors.New("text element of plain text empty") + } + + return nil +} diff --git a/source.go b/source.go new file mode 100644 index 0000000..e1e8e84 --- /dev/null +++ b/source.go @@ -0,0 +1,112 @@ +package atomfeed + +import "fmt" + +type Source struct { + *CommonAttributes + Authors []*Person `xml:"author,omitempty"` + Categories []*Category `xml:"category,omitempty"` + Contributors []*Person `xml:"contributor,omitempty"` + Generator *Generator `xml:"generator,omitempty"` + Icon *Icon `xml:"icon,omitempty"` + ID *ID `xml:"id,omitempty"` + Links []*Link `xml:"link,omitempty"` + Logo *Logo `xml:"logo,omitempty"` + Rights *Text `xml:"rights,omitempty"` + Subtitle *Text `xml:"subtitle,omitempty"` + Title *Text `xml:"title,omitempty"` + Updated *Date `xml:"updated,omitempty"` + Extensions []*ExtensionElement `xml:",any"` +} + +func (s *Source) Check() error { + if s.Authors != nil { + for i, a := range s.Authors { + if err := a.Check(); err != nil { + return fmt.Errorf("author element %v of source: %v", i, err) + } + } + } + + if s.Categories != nil { + for i, c := range s.Categories { + if err := c.Check(); err != nil { + return fmt.Errorf("category element %v of source: %v", i, err) + } + } + } + + if s.Contributors != nil { + for i, c := range s.Contributors { + if err := c.Check(); err != nil { + return fmt.Errorf("contributor element %v of source: %v", i, err) + } + } + } + + if s.Generator != nil { + if err := s.Generator.Check(); err != nil { + return fmt.Errorf("generator element of source: %v", err) + } + } + + if s.Icon != nil { + if err := s.Icon.Check(); err != nil { + return fmt.Errorf("icon element of source: %v", err) + } + } + + if s.ID != nil { + if err := s.ID.Check(); err != nil { + return fmt.Errorf("id element of source: %v", err) + } + } + + if s.Links != nil { + for i, l := range s.Links { + if err := l.Check(); err != nil { + return fmt.Errorf("link element %v of source: %v", i, err) + } + } + } + + if s.Logo != nil { + if err := s.Logo.Check(); err != nil { + return fmt.Errorf("logo element of source: %v", err) + } + } + + if s.Rights != nil { + if err := (*s.Rights).Check(); err != nil { + return fmt.Errorf("rights element of source: %v", err) + } + } + + if s.Subtitle != nil { + if err := (*s.Subtitle).Check(); err != nil { + return fmt.Errorf("subtitle element of source: %v", err) + } + } + + if s.Title != nil { + if err := (*s.Title).Check(); err != nil { + return fmt.Errorf("title element of source: %v", err) + } + } + + if s.Updated != nil { + if err := s.Updated.Check(); err != nil { + return fmt.Errorf("updated element of source: %v", err) + } + } + + if s.Extensions != nil { + for i, e := range s.Extensions { + if err := e.Check(); err != nil { + return fmt.Errorf("extension element %v of source: %v", i, err) + } + } + } + + return nil +} diff --git a/text.go b/text.go new file mode 100644 index 0000000..2a5ec6c --- /dev/null +++ b/text.go @@ -0,0 +1,6 @@ +package atomfeed + +type Text interface { + Check() error + IsText() bool +} diff --git a/xhtmlText.go b/xhtmlText.go new file mode 100644 index 0000000..59139a4 --- /dev/null +++ b/xhtmlText.go @@ -0,0 +1,23 @@ +package atomfeed + +import "errors" + +type XHTMLText struct { + *CommonAttributes + Type string `xml:"type,attr"` // Must be xhtml + XHTMLDiv string `xml:"div"` +} + +func (x *XHTMLText) IsText() bool { return true } + +func (x *XHTMLText) Check() error { + if x.Type != "xhtml" { + return errors.New("type attribute of xhtml text must be xhtml") + } + + if x.XHTMLDiv == "" { + return errors.New("xhtmlDiv element of xhtml text empty") + } + + return nil +}