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 +}