55 Commits

Author SHA1 Message Date
c5a6e0c44f Change module description in go.mod 2024-10-16 21:08:37 +02:00
3774f41f1b Changed README 2024-10-16 20:35:44 +02:00
7a2e79d205 Corrected LICENSE 2024-10-16 20:30:02 +02:00
e12cd132d9 Small comment clean up 2024-10-16 20:24:43 +02:00
03ec0d2ddf Remove unused stuff 2024-10-16 20:23:52 +02:00
4220a2acc6 Added and corrected comments 2024-10-16 19:59:28 +02:00
6a76bdc0c3 Small changes 2024-10-16 18:41:42 +02:00
31cc803440 Create NewLogo and add checks 2024-10-16 18:33:23 +02:00
f11dee630d Make isText private 2024-10-16 18:31:24 +02:00
f8c36a7045 Change title to string type and add checks 2024-10-16 18:31:13 +02:00
4fe133a394 Create isValidMediaType and isValidLanguageTag 2024-10-16 18:30:22 +02:00
39bd0776c5 Correct AddExtension for entry and person 2024-10-16 17:38:03 +02:00
082e71a698 Change URI to IRI after finding validation pattern 2024-10-16 17:33:25 +02:00
b76e529ca3 Created NewLink 2024-10-16 17:17:41 +02:00
bc9fd49d18 Add checks to id 2024-10-16 17:07:57 +02:00
a65aa0a740 Create NewIcon and appropriate checks 2024-10-16 17:03:23 +02:00
566227773e Create NewGenerator and checks for the generator construct 2024-10-16 16:58:58 +02:00
92d71fdbde Fix wordings 2024-10-16 16:54:26 +02:00
f27116930a Correctly escape strings if needed and check for it 2024-10-16 16:51:39 +02:00
c200d5bf73 Create isValidURN and isValidURI and use isValidURI everywhere where it is needed 2024-10-16 16:48:44 +02:00
14696371e2 Create and use isValidURL 2024-10-16 16:14:34 +02:00
4f70da9a17 Add NewCategory 2024-10-15 22:17:10 +02:00
1f7f7966ef Add check for whether a mime type is used for inline other and out of line content 2024-10-15 21:47:13 +02:00
7764589dd3 Add NewContent 2024-10-15 21:14:30 +02:00
9df834d927 Add newOutOfLineContent 2024-10-15 21:13:46 +02:00
ea79900bf3 Correct typo 2024-10-15 21:10:12 +02:00
b26d6370f5 Add newInlineOtherContent 2024-10-15 21:01:40 +02:00
15b74c2675 Add newInlineXHTMLContent 2024-10-15 20:53:01 +02:00
d96e2c61bb Just moving code around, no change 2024-10-15 20:52:35 +02:00
31b6e51cb8 Add newInlineTextContent 2024-10-15 20:47:16 +02:00
4bad8ae99f Allow for empty type in NewText 2024-10-15 20:09:18 +02:00
068f61dc2c Simplify NewText 2024-10-15 20:03:09 +02:00
987feb8226 Check if the type of contents is a composite media type 2024-10-15 19:53:17 +02:00
6322566a54 Add function to check if the media type is composite 2024-10-15 19:46:45 +02:00
209059f2b4 Move the parsing of the media type to isXMLMediaType 2024-10-15 19:46:26 +02:00
e3b9ff0225 Move isXMLMediaType to atom.go 2024-10-15 19:40:13 +02:00
b08b62e794 Add check for summary element of entry 2024-10-15 19:32:14 +02:00
9920e077eb Add hasAlternateDuplicateLinks check to entry 2024-10-15 18:46:19 +02:00
6782d0c847 Move hasAlternateDumplicateLinks from feed to link in order to also use it elsewhere 2024-10-15 18:44:02 +02:00
656ae8ad46 Add comments to author checks 2024-10-15 18:40:32 +02:00
4f3135dcc6 Add comment to alternateRelExists check 2024-10-15 18:37:54 +02:00
f6bbc2fa6f Delete unnecessary nil checks from entry 2024-10-15 18:35:53 +02:00
1ecda50f34 Check whether there is a link element with a rel attribute of "alternate" if there is no content element in an entry element 2024-10-15 18:34:19 +02:00
cd61cb4f27 Delete unnecessary nil checks from feed 2024-10-15 18:32:24 +02:00
f2c6fce7c9 Text constructs should not be pointed towards since they are pointers themselves 2024-10-15 18:07:06 +02:00
c5783268f8 Adapt comments in feed.go and and entry.go 2024-10-15 18:04:03 +02:00
4d35873625 Corrected error message for link check 2024-10-15 17:54:54 +02:00
05cc967ea8 Check for links with "alternate" ref and duplicate type and hreflang attributes 2024-10-15 17:52:38 +02:00
e1ba1b8277 Update updated of feed when adding an object 2024-10-15 17:39:17 +02:00
8863e97f1f Remove unnecessary updated time from NewFeed 2024-10-15 17:36:56 +02:00
215a002992 Small change 2024-10-15 17:35:48 +02:00
a021de6e66 Adapt feed comments 2024-10-15 17:34:33 +02:00
16d8b577e3 Add functions to create a new feed and add objects to slices 2024-10-15 17:29:22 +02:00
ac78db9917 Add function to create a new extension element 2024-10-15 17:26:59 +02:00
9ec6ad1460 Add function to create a new ID 2024-10-15 17:26:26 +02:00
27 changed files with 727 additions and 177 deletions

View File

@ -208,8 +208,8 @@ If you develop a new program, and you want it to be of the greatest possible use
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
atom-feed
Copyright (C) 2024 jason
atom
Copyright (C) 2024 Jason Streifling
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
atom-feed Copyright (C) 2024 jason
atom Copyright (C) 2024 Jason Streifling
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.

View File

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

66
atom.go
View File

@ -1,8 +1,72 @@
package atomfeed
import (
"mime"
"regexp"
"strings"
"golang.org/x/text/language"
)
type (
EmailAddress string
LanguageTag string
MediaType string
URI string
IRI string
)
// 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 IRI) bool {
pattern := `((([A-Za-z])[A-Za-z0-9+\-\.]*):((//(((([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽!$&'()*+,;=:]|(%[0-9A-Fa-f][0-9A-Fa-f]))*@))?((\[((((([0-9A-Fa-f]{0,4}:)){6}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|(::(([0-9A-Fa-f]{0,4}:)){5}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|(([0-9A-Fa-f]{0,4})?::(([0-9A-Fa-f]{0,4}:)){4}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:))?[0-9A-Fa-f]{0,4}))?::(([0-9A-Fa-f]{0,4}:)){3}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:)){0,2}[0-9A-Fa-f]{0,4}))?::(([0-9A-Fa-f]{0,4}:)){2}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:)){0,3}[0-9A-Fa-f]{0,4}))?::[0-9A-Fa-f]{0,4}:(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:)){0,4}[0-9A-Fa-f]{0,4}))?::(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:)){0,5}[0-9A-Fa-f]{0,4}))?::[0-9A-Fa-f]{0,4})|((((([0-9A-Fa-f]{0,4}:)){0,6}[0-9A-Fa-f]{0,4}))?::))|(v[0-9A-Fa-f]+\.[A-Za-z0-9\-\._~!$&'()*+,;=:]+))\])|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))|(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=]))*)((:[0-9]*))?)((/(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))*))*)|(/(((([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))+((/(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))*))*))?)|((([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))+((/(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))*))*)|)((\?(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@])|[-󰀀-󿿽􀀀-􏿽/?])*))?((#((([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@])|/|\?))*))?)`
return regexp.MustCompile(pattern).MatchString(string(iri))
}
// isCorrectlyEscaped checks whether a string is correctly escaped as per
// RFC4287. It returns a bool.
func isCorrectlyEscaped(text string) bool {
relevantEntities := []string{"&", "<", ">", """, "'"}
for _, entity := range relevantEntities {
if strings.Contains(text, entity) {
return false
}
}
return true
}
// isCompositeMediaType checks whether a string is a composite media type. It
// returns a bool.
func isCompositeMediaType(mediaType string) bool {
mediaType, _, err := mime.ParseMediaType(mediaType)
if err != nil {
return false
}
return strings.HasPrefix(mediaType, "multipart/") || strings.HasPrefix(mediaType, "message/")
}
// isXMLMediaType checks whether a string is an xml media type. It returns a
// bool.
func isXMLMediaType(mediaType string) bool {
mediaType, _, err := mime.ParseMediaType(mediaType)
if err != nil {
return false
}
return strings.HasSuffix(mediaType, "/xml") || strings.HasSuffix(mediaType, "+xml")
}
// isValidMediaType checks whether a string is a valid media type. It returns a
// bool.
func isValidMediaType(mediaType string) bool {
_, _, err := mime.ParseMediaType(mediaType)
return err == nil
}
// isValidLanguageTag checks whether a LanguageTag is valid. It returns a bool.
func isValidLanguageTag(tag LanguageTag) bool {
_, err := language.Parse(string(tag))
return err == nil
}

View File

@ -3,25 +3,53 @@ package atomfeed
import (
"errors"
"fmt"
"html"
)
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"`
Content Content `xml:"content"` // undefinedContent in RFC4287
Term string `xml:"term,attr"`
Scheme IRI `xml:"scheme,attr,omitempty"`
Label string `xml:"label,attr,omitempty"`
}
// NewCategory creates a new Category. It returns a *Category and an error.
func NewCategory(term string) (*Category, error) {
content, err := NewContent(InlineText, "", "")
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.
func (c *Category) SetLabel(label string) {
c.Label = html.UnescapeString(label)
}
// Check checks the Category for incompatibilities with RFC4287. It returns an
// error.
func (c *Category) Check() error {
if c.Term == "" {
return errors.New("term attribute of category empty")
}
if c.Scheme != "" {
if !isValidIRI(c.Scheme) {
return fmt.Errorf("scheme attribute %v of category not correctly formatted", c.Scheme)
}
}
if !isCorrectlyEscaped(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 {
if err := c.Content.Check(); err != nil {
return fmt.Errorf("content element of category: %v", err)
}
}

View File

@ -3,11 +3,13 @@ package atomfeed
import "fmt"
type CommonAttributes struct {
Base URI `xml:"base,attr,omitempty"`
Base IRI `xml:"base,attr,omitempty"`
Lang LanguageTag `xml:"lang,attr,omitempty"`
UndefinedAttributes []*ExtensionAttribute `xml:",any"`
}
// Check checks the CommonAttributes for incompatibilities with RFC4287. It
// returns an error.
func (c *CommonAttributes) Check() error {
for i, e := range c.UndefinedAttributes {
if err := e.Check(); err != nil {

View File

@ -1,6 +1,35 @@
package atomfeed
import (
"fmt"
)
const (
InlineText = iota
InlineXHTML
InlineOther
OutOfLine
)
type Content interface {
isContent() bool
hasSRC() bool
getType() string
Check() error
IsContent() bool
}
// NewContent creates a new Content. It returns a Content and an error.
func NewContent(contentType int, mediaType string, content any) (Content, error) {
switch contentType {
case 0:
return newInlineTextContent(mediaType, content)
case 1:
return newInlineXHTMLContent(mediaType, content)
case 2:
return newInlineOtherContent(mediaType, content)
case 3:
return newOutOfLineContent(mediaType, content)
default:
return nil, fmt.Errorf("%v is not a valid text type", contentType)
}
}

View File

@ -10,14 +10,19 @@ type Date struct {
DateTime string
}
// DateTime formats a time.Time to string formated as defined by RFC3339. It
// returns a string.
func DateTime(t time.Time) string {
return string(t.Format(time.RFC3339))
}
// NewDate creates a new Date. It returns a *Date.
func NewDate(t time.Time) *Date {
return &Date{DateTime: DateTime(t)}
}
// Check checks the Date for incompatibilities with RFC4287. It returns an
// error.
func (d *Date) Check() error {
if d.DateTime == "" {
return errors.New("date time element of date is empty")

119
entry.go
View File

@ -1,28 +1,39 @@
package atomfeed
import (
"encoding/xml"
"errors"
"fmt"
"strings"
"time"
)
// It is advisable that each atom:entry element contain a non-empty atom:title
// element, a non-empty atom:content element when that element is present, and
// a non-empty atom:summary element when the entry contains no atom:content
// element.
type Entry struct {
*CommonAttributes
Authors []*Person `xml:"author,omitempty"`
Categories []*Category `xml:"category,omitempty"`
Content *Content `xml:"content,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"`
Rights Text `xml:"rights,omitempty"`
Source *Source `xml:"source,omitempty"`
Summary *Text `xml:"summary,omitempty"`
Title *Text `xml:"title"`
Summary Text `xml:"summary,omitempty"`
Title Text `xml:"title"`
Updated *Date `xml:"updated"`
Extensions []*ExtensionElement `xml:",any,omitempty"`
}
// checkAuthors checks the entry's authors for incompatibilities with RFC4287.
// It returns an errors.
// atom:entry elements MUST contain one or more atom:author elements, unless
// 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
// atom:author element itself.
func (e *Entry) checkAuthors() error {
if e.Authors == nil {
if e.Source.Authors == nil {
@ -39,10 +50,34 @@ func (e *Entry) checkAuthors() error {
return nil
}
func (e *Entry) AddExtension(name string, value any) {
e.Extensions = append(e.Extensions, &ExtensionElement{XMLName: xml.Name{Local: name}, Value: value})
// 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)
}
return &Entry{
ID: NewID(),
Title: text,
Updated: NewDate(time.Now()),
}, nil
}
// AddExtension adds the ExtensionElement to the Entry.
func (e *Entry) AddExtension(x *ExtensionElement) {
if e.Extensions == nil {
e.Extensions = make([]*ExtensionElement, 1)
e.Extensions[0] = x
} else {
e.Extensions = append(e.Extensions, x)
}
e.Updated.DateTime = DateTime(time.Now())
}
// Check checks the Entry for incompatibilities with RFC4287. It returns an
// error.
func (e *Entry) Check() error {
if e.ID == nil {
return errors.New("no id element of entry")
@ -56,36 +91,40 @@ func (e *Entry) Check() error {
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)
}
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 {
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)
}
} 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 errors.New("no content element of entry %v and no link element with rel \"alternate\"")
}
}
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)
}
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)
}
}
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 hasAlternateDuplicateLinks(e.Links) {
return fmt.Errorf("links with 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.ID.URI, err)
@ -93,7 +132,7 @@ func (e *Entry) Check() error {
}
if e.Rights != nil {
if err := (*e.Rights).Check(); err != nil {
if err := e.Rights.Check(); err != nil {
return fmt.Errorf("rights element of entry %v: %v", e.ID.URI, err)
}
}
@ -105,15 +144,31 @@ func (e *Entry) Check() error {
}
if e.Summary != nil {
if err := (*e.Summary).Check(); err != nil {
if err := e.Summary.Check(); err != nil {
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
// of the following cases:
// 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.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
// is not an XML media type [RFC3023], does not begin with "text/", and
// does not end with "/xml" or "+xml".
mediaType := e.Content.getType()
if !isXMLMediaType(mediaType) && !strings.HasPrefix(mediaType, "text/") {
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.ID.URI)
} else {
if err := (*e.Title).Check(); err != nil {
if err := e.Title.Check(); err != nil {
return fmt.Errorf("title element of entry %v: %v", e.ID.URI, err)
}
}
@ -126,11 +181,9 @@ func (e *Entry) Check() error {
}
}
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)
}
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)
}
}

View File

@ -10,6 +10,8 @@ type ExtensionAttribute struct {
XMLName xml.Name
}
// Check checks the ExtensionAttribute for incompatibilities with RFC4287. It
// returns an error.
func (e *ExtensionAttribute) Check() error {
if e.Value == nil {
return errors.New("value element of extension attribute empty")

View File

@ -10,6 +10,14 @@ type ExtensionElement struct {
XMLName xml.Name
}
// NewExtensionElement creates a new ExtensionElement. It returns a
// *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.Value == nil {
return errors.New("value element of extension element empty")

169
feed.go
View File

@ -4,6 +4,7 @@ import (
"encoding/xml"
"errors"
"fmt"
"time"
)
type Feed struct {
@ -15,20 +16,104 @@ type Feed struct {
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"
Links []*Link `xml:"link,omitempty"`
Logo *Logo `xml:"logo,omitempty"`
Rights *Text `xml:"rights,omitempty"`
Subtitle *Text `xml:"subtitle,omitempty"`
Title *Text `xml:"title"`
Rights Text `xml:"rights,omitempty"`
Subtitle Text `xml:"subtitle,omitempty"`
Title Text `xml:"title"`
Updated *Date `xml:"updated"`
Extensions []*ExtensionElement `xml:",any,omitempty"`
Entries []*Entry `xml:"entry,omitempty"`
}
func (f *Feed) AddExtension(name string, value any) {
f.Extensions = append(f.Extensions, &ExtensionElement{XMLName: xml.Name{Local: name}, Value: value})
// 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)
}
return &Feed{
ID: NewID(),
Title: text,
Updated: NewDate(time.Now()),
}, nil
}
// AddAuthor adds the Person as an author to the Feed.
func (f *Feed) AddAuthor(p *Person) {
if f.Authors == nil {
f.Authors = make([]*Person, 1)
f.Authors[0] = p
} else {
f.Authors = append(f.Authors, p)
}
f.Updated.DateTime = DateTime(time.Now())
}
// AddCategory adds the Category to the Feed.
func (f *Feed) AddCategory(c *Category) {
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())
}
// AddContributor adds the Person as a contributor to the Feed.
func (f *Feed) AddContributor(c *Person) {
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())
}
// AddLink adds the Link to the Feed. There should be one Link with Rel "self".
func (f *Feed) AddLink(l *Link) {
if f.Links == nil {
f.Links = make([]*Link, 1)
f.Links[0] = l
} else {
f.Links = append(f.Links, l)
}
f.Updated.DateTime = DateTime(time.Now())
}
// AddExtension adds the Extension to the Feed.
func (f *Feed) AddExtension(e *ExtensionElement) {
if f.Extensions == nil {
f.Extensions = make([]*ExtensionElement, 1)
f.Extensions[0] = e
} else {
f.Extensions = append(f.Extensions, e)
}
f.Updated.DateTime = DateTime(time.Now())
}
// AddEntry adds the Entry to the Feed.
func (f *Feed) AddEntry(e *Entry) {
if f.Entries == nil {
f.Entries = make([]*Entry, 1)
f.Entries[0] = e
} else {
f.Entries = append(f.Entries, e)
}
f.Updated.DateTime = DateTime(time.Now())
}
// Check checks the Feed for incompatibilities with RFC4287. It returns an
// error.
func (f *Feed) Check() error {
if f.ID == nil {
return errors.New("no id element of feed")
@ -38,6 +123,9 @@ func (f *Feed) Check() error {
}
}
// atom:feed elements MUST contain one or more atom:author elements, unless
// all of the atom:feed element's child atom:entry elements contain at
// least one atom:author element.
if f.Authors == nil {
for _, e := range f.Entries {
if err := e.checkAuthors(); err != nil {
@ -52,19 +140,15 @@ func (f *Feed) Check() error {
}
}
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)
}
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)
}
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)
}
}
@ -80,13 +164,14 @@ func (f *Feed) Check() error {
}
}
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)
}
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 hasAlternateDuplicateLinks(f.Links) {
return fmt.Errorf("links with 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 {
@ -95,13 +180,13 @@ func (f *Feed) Check() error {
}
if f.Rights != nil {
if err := (*f.Rights).Check(); err != 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 {
if err := f.Subtitle.Check(); err != nil {
return fmt.Errorf("subtitle element of feed %v: %v", f.ID.URI, err)
}
}
@ -109,7 +194,7 @@ func (f *Feed) Check() error {
if f.Title == nil {
return fmt.Errorf("no title element of feed %v", f.ID.URI)
} else {
if err := (*f.Title).Check(); err != nil {
if err := f.Title.Check(); err != nil {
return fmt.Errorf("title element of feed %v: %v", f.ID.URI, err)
}
}
@ -122,44 +207,22 @@ func (f *Feed) Check() error {
}
}
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)
}
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)
}
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"})
}
}
}
// ToXML converts the Feed to XML. It returns a string and an error.
func (f *Feed) ToXML(encoding string) (string, error) {
xml, err := xml.MarshalIndent(f, "", " ")
if err != nil {

View File

@ -1,18 +1,39 @@
package atomfeed
import "errors"
import (
"errors"
"fmt"
"html"
)
type Generator struct {
*CommonAttributes
URI URI `xml:"uri,attr,omitempty"`
URI IRI `xml:"uri,attr,omitempty"`
Version string `xml:"version,attr,omitempty"`
Text string `xml:"text"`
}
// NewGenerator creates a new Generator. It returns a *Generator.
func NewGenerator(text string) *Generator {
return &Generator{Text: html.UnescapeString(text)}
}
// Check checks the Generator for incompatibilities with RFC4287. It returns an
// error.
func (g *Generator) Check() error {
if g.URI != "" {
if !isValidIRI(g.URI) {
return fmt.Errorf("uri attribute %v of generator not correctly formatted", g.URI)
}
}
if g.Text == "" {
return errors.New("text element of generator empty")
}
if !isCorrectlyEscaped(g.Text) {
return fmt.Errorf("text element %v of generator not correctly escaped", g.Text)
}
return nil
}

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module git.streifling.com/jason/atom
go 1.23.2
require (
github.com/google/uuid v1.6.0
golang.org/x/text v0.19.0
)

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=

18
icon.go
View File

@ -1,15 +1,29 @@
package atomfeed
import "errors"
import (
"errors"
"fmt"
)
type Icon struct {
*CommonAttributes
URI URI `xml:"uri"`
URI IRI `xml:"uri"`
}
// NewIcon creates a new Icon. It returns a *Icon.
func NewIcon(uri string) *Icon {
return &Icon{URI: IRI(uri)}
}
// Check checks the Icon for incompatibilities with RFC4287. It returns an
// error.
func (i *Icon) Check() error {
if i.URI == "" {
return errors.New("uri element of icon empty")
} else {
if !isValidIRI(i.URI) {
return fmt.Errorf("uri attribute %v of icon not correctly formatted", i.URI)
}
}
return nil

19
id.go
View File

@ -1,15 +1,30 @@
package atomfeed
import "errors"
import (
"errors"
"fmt"
"github.com/google/uuid"
)
type ID struct {
*CommonAttributes
URI URI `xml:"uri"`
URI IRI `xml:"uri"`
}
// NewID creates a new ID. It returns a *ID.
func NewID() *ID {
return &ID{URI: IRI(fmt.Sprint("urn:uuid:", uuid.New()))}
}
// Check checks the ID for incompatibilities with RFC4287. It returns an error.
func (i *ID) Check() error {
if i.URI == "" {
return errors.New("uri element of id empty")
} else {
if !isValidIRI(i.URI) {
return fmt.Errorf("uri element %v of id not correctly formatted", i.URI)
}
}
return nil

View File

@ -1,11 +1,50 @@
package atomfeed
import (
"errors"
"fmt"
"mime"
)
type InlineOtherContent struct {
*CommonAttributes
AnyElement any `xml:"anyelement,omitempty"`
Type MediaType `xml:"type,attr,omitempty"`
AnyElement []*any `xml:"anyelement,omitempty"`
}
func (i *InlineOtherContent) IsContent() bool { return true }
// newInlineOtherContent creates a new InlineOtherContent. It returns a
// *InlineOtherContent and an error.
func newInlineOtherContent(mediaType string, content any) (*InlineOtherContent, error) {
if mediaType, _, err := mime.ParseMediaType(mediaType); err != nil {
return nil, fmt.Errorf("media type %v incompatible with inline other content", mediaType)
}
func (i *InlineOtherContent) Check() error { return nil }
return &InlineOtherContent{Type: MediaType(mediaType), AnyElement: content}, nil
}
// isContent checks whether the InlineOtherContent is a Content. It returns a
// bool.
func (i *InlineOtherContent) isContent() bool { return true }
// hasSRC checks whether the InlineOtherContent has a SRC attribute. It returns
// a bool.
func (i *InlineOtherContent) hasSRC() bool { return false }
// getType returns the Type of the InlineOtherContent as a string.
func (i *InlineOtherContent) getType() string { return string(i.Type) }
// Check checks the InlineOtherContent for incompatibilities with RFC4287. It
// returns an error.
func (i *InlineOtherContent) Check() error {
mediaType := i.getType()
if mediaType, _, err := mime.ParseMediaType(mediaType); err != nil {
return fmt.Errorf("type attribute %v incompatible with inline other content", mediaType)
}
if isCompositeMediaType(mediaType) {
return errors.New("type attribute of inline other content must not be a composite type")
}
return nil
}

View File

@ -1,6 +1,10 @@
package atomfeed
import "errors"
import (
"errors"
"fmt"
"reflect"
)
type InlineTextContent struct {
*CommonAttributes
@ -8,8 +12,45 @@ type InlineTextContent struct {
Texts []string `xml:"texts,omitempty"`
}
func (i *InlineTextContent) IsContent() bool { return true }
// 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)
}
texts := make([]string, 0)
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 &InlineTextContent{Type: mediaType, Texts: texts}, nil
}
// isContent checks whether the InlineTextContent is a Content. It returns a
// bool.
func (i *InlineTextContent) isContent() bool { return true }
// hasSRC checks whether the InlineTextContent has a SRC attribute. It returns
// a bool.
func (i *InlineTextContent) hasSRC() bool { return false }
// getType returns the Type of the InlineTextContent as a string.
func (i *InlineTextContent) getType() string { return i.Type }
// Check checks the InlineTextContent for incompatibilities with RFC4287. It
// returns an error.
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")

View File

@ -1,6 +1,10 @@
package atomfeed
import "errors"
import (
"errors"
"fmt"
"reflect"
)
type InlineXHTMLContent struct {
*CommonAttributes
@ -8,8 +12,33 @@ type InlineXHTMLContent struct {
XHTMLDiv string `xml:"xhtmldiv"`
}
func (i *InlineXHTMLContent) IsContent() bool { return true }
// 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)
}
if reflect.TypeOf(content).Kind() != reflect.String {
return nil, fmt.Errorf("content type %T incompatible with inline xhtml content", content)
}
return &InlineXHTMLContent{Type: mediaType, XHTMLDiv: content.(string)}, nil
}
// isContent checks whether the InlineXHTMLContent is a Content. It returns a
// bool.
func (i *InlineXHTMLContent) isContent() bool { return true }
// hasSRC checks whether the InlineXHTMLContent has a SRC attribute. It returns
// a bool.
func (i *InlineXHTMLContent) hasSRC() bool { return false }
// getType returns the Type of the InlineXHTMLContent as a string.
func (i *InlineXHTMLContent) getType() string { return i.Type }
// Check checks the InlineXHTMLContent for incompatibilities with RFC4287. It
// returns an error.
func (i *InlineXHTMLContent) Check() error {
if i.Type != "xhtml" {
return errors.New("type attribute of inline xhtml content must be xhtml")

72
link.go
View File

@ -3,37 +3,93 @@ package atomfeed
import (
"errors"
"fmt"
"strings"
)
type Link struct {
*CommonAttributes
Title *Text `xml:"title,attr,omitempty"`
Content *Content `xml:"content"`
Href URI `xml:"href,attr"`
Title string `xml:"title,attr,omitempty"`
Content Content `xml:"content"` // undefinedContent in RFC4287
Href IRI `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"`
}
// NewLink creates a new Link. It returns a *Link and an error.
func NewLink(href string) (*Link, error) {
content, err := NewContent(InlineText, "", "")
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
// error.
func (l *Link) Check() error {
if l.Href == "" {
return errors.New("href attribute of link empty")
} else {
if !isValidIRI(l.Href) {
return fmt.Errorf("href attribute %v of link not correctly formatted", l.Href)
}
}
if l.Title != nil {
if err := (*l.Title).Check(); err != nil {
return fmt.Errorf("title attribute of link %v: %v", l.Href, err)
}
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)) {
return fmt.Errorf("type attribute %v of link %v invalid media type", l.Type, l.Href)
}
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 {
if err := l.Content.Check(); err != nil {
return fmt.Errorf("content element of link %v: %v", l.Href, err)
}
}
return nil
}
// hasAlternateDuplicateLinks checks whether multiple Links with Rel
// "alternate" also have Type and HrefLang in common. It returns a bool.
// atom:feed/entry elements MUST NOT contain more than one atom:link element
// with a rel attribute value of "alternate" that has the same combination of
// type and hreflang attribute values.
func hasAlternateDuplicateLinks(l []*Link) bool {
linkMap := make(map[string]bool)
for _, link := range l {
if link.Rel == "alternate" {
key := fmt.Sprint(link.Type, "|", link.HrefLang)
if linkMap[key] {
return true
}
linkMap[key] = true
}
}
return false
}
// alternateRelExists checks whether multiple Links with Rel "alternate" exist.
// It returns a bool.
func alternateRelExists(l []*Link) bool {
for _, link := range l {
if link.Rel == "alternate" {
return true
}
}
return false
}

20
logo.go
View File

@ -1,15 +1,31 @@
package atomfeed
import "errors"
import (
"errors"
"fmt"
"github.com/google/uuid"
)
type Logo struct {
*CommonAttributes
URI URI `xml:"uri"`
URI IRI `xml:"uri"`
}
// NewLogo creates a new Logo. It returns a *Logo.
func NewLogo() *Logo {
return &Logo{URI: IRI(fmt.Sprint("urn:uuid:", uuid.New()))}
}
// Check checks the Logo for incompatibilities with RFC4287. It returns an
// error.
func (l *Logo) Check() error {
if l.URI == "" {
return errors.New("uri element of logo empty")
} else {
if !isValidIRI(l.URI) {
return fmt.Errorf("uri element %v of logo not correctly formatted", l.URI)
}
}
return nil

View File

@ -1,16 +1,60 @@
package atomfeed
import "errors"
import (
"errors"
"fmt"
"mime"
"reflect"
)
type OutOfLineContent struct {
*CommonAttributes
Type MediaType `xml:"type,attr,omitempty"`
SRC URI `xml:"src,attr"`
SRC IRI `xml:"src,attr"`
}
func (o *OutOfLineContent) IsContent() bool { return true }
// newOutOfLineContent creates a new OutOfLineContent. It returns a
// *OutOfLineContent and an error.
func newOutOfLineContent(mediaType string, content any) (*OutOfLineContent, error) {
if mediaType, _, err := mime.ParseMediaType(mediaType); err != nil {
return nil, fmt.Errorf("media type %v incompatible with out of line content", mediaType)
}
if reflect.TypeOf(content).Kind() != reflect.String {
return nil, fmt.Errorf("content type %T incompatible with out of line content", content)
}
if !isValidIRI(content.(IRI)) {
return nil, errors.New("content not a valid uri")
}
return &OutOfLineContent{Type: MediaType(mediaType), SRC: content.(IRI)}, nil
}
// isContent checks whether the OutOfLineContent is a Content. It returns a
// bool.
func (o *OutOfLineContent) isContent() bool { return true }
// hasSRC checks whether the OutOfLineContent has a SRC attribute. It returns a
// bool.
func (o *OutOfLineContent) hasSRC() bool { return true }
// getType returns the Type of the OutOfLineContent as a string.
func (o *OutOfLineContent) getType() string { return string(o.Type) }
// Check checks the OutOfLineContent for incompatibilities with RFC4287. It
// returns an error.
func (o *OutOfLineContent) Check() error {
mediaType := o.getType()
if mediaType, _, err := mime.ParseMediaType(mediaType); err != nil {
return fmt.Errorf("type attribute %v incompatible with out of line content", mediaType)
}
if isCompositeMediaType(mediaType) {
return errors.New("type attribute of out of line content must not be a composite type")
}
if o.SRC == "" {
return errors.New("src attribute of out of line content empty")
}

View File

@ -1,37 +1,44 @@
package atomfeed
import (
"encoding/xml"
"errors"
"fmt"
"net/mail"
"net/url"
)
type Person struct {
*CommonAttributes
Name string `xml:"name"`
URI URI `xml:"uri,omitempty"`
URI IRI `xml:"uri,omitempty"`
Email EmailAddress `xml:"email,omitempty"`
Extensions []*ExtensionElement `xml:",any,omitempty"`
}
// NewPerson creates a new Person. It returns a *Person.
func NewPerson(name string) *Person {
return &Person{Name: name}
}
func (p *Person) AddExtension(name string, value any) {
p.Extensions = append(p.Extensions, &ExtensionElement{XMLName: xml.Name{Local: name}, Value: value})
// AddExtension adds the Extension to the Person.
func (p *Person) AddExtension(e *ExtensionElement) {
if p.Extensions == nil {
p.Extensions = make([]*ExtensionElement, 1)
p.Extensions[0] = e
} else {
p.Extensions = append(p.Extensions, e)
}
}
// Check checks the Person for incompatibilities with RFC4287. It returns an
// error.
func (p *Person) Check() error {
if p.Name == "" {
return errors.New("name element of person element empty")
}
if p.URI != "" {
if _, err := url.ParseRequestURI(string(p.URI)); err != nil {
return fmt.Errorf("email element of person %v not correctly formatted", p.Name)
if !isValidIRI(p.URI) {
return fmt.Errorf("uri element of person %v not correctly formatted", p.Name)
}
}

View File

@ -8,13 +8,20 @@ type PlainText struct {
Text string `xml:"text"`
}
func (p *PlainText) IsText() bool { return true }
// isText checks whether the PlainText is a Text. It returns a bool.
func (p *PlainText) isText() bool { return true }
// Check checks the PlainText for incompatibilities with RFC4287. It returns an
// error.
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.Type == "html" && !isCorrectlyEscaped(p.Text) {
return errors.New("text element of plain text not correctly escaped")
}
if p.Text == "" {
return errors.New("text element of plain text empty")
}

View File

@ -12,99 +12,91 @@ type Source struct {
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"`
Rights Text `xml:"rights,omitempty"`
Subtitle Text `xml:"subtitle,omitempty"`
Title Text `xml:"title,omitempty"`
Updated *Date `xml:"updated,omitempty"`
Extensions []*ExtensionElement `xml:",any,omitempty"`
}
// Check checks the Source for incompatibilities with RFC4287. It returns an
// error.
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)
}
for i, a := range s.Authors {
if err := a.Check(); err != nil {
return fmt.Errorf("author element %v of source %v: %v", i, s.ID.URI, 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)
}
for i, c := range s.Categories {
if err := c.Check(); err != nil {
return fmt.Errorf("category element %v of source %v: %v", i, s.ID.URI, 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)
}
for i, c := range s.Contributors {
if err := c.Check(); err != nil {
return fmt.Errorf("contributor element %v of source %v: %v", i, s.ID.URI, err)
}
}
if s.Generator != nil {
if err := s.Generator.Check(); err != nil {
return fmt.Errorf("generator element of source: %v", err)
return fmt.Errorf("generator element of source %v: %v", s.ID.URI, err)
}
}
if s.Icon != nil {
if err := s.Icon.Check(); err != nil {
return fmt.Errorf("icon element of source: %v", err)
return fmt.Errorf("icon element of source %v: %v", s.ID.URI, err)
}
}
if s.ID != nil {
if err := s.ID.Check(); err != nil {
return fmt.Errorf("id element of source: %v", err)
return fmt.Errorf("id element of source %v: %v", s.ID.URI, 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)
}
for i, l := range s.Links {
if err := l.Check(); err != nil {
return fmt.Errorf("link element %v of source %v: %v", i, s.ID.URI, err)
}
}
if s.Logo != nil {
if err := s.Logo.Check(); err != nil {
return fmt.Errorf("logo element of source: %v", err)
return fmt.Errorf("logo element of source %v: %v", s.ID.URI, err)
}
}
if s.Rights != nil {
if err := (*s.Rights).Check(); err != nil {
return fmt.Errorf("rights element of source: %v", err)
if err := s.Rights.Check(); err != nil {
return fmt.Errorf("rights element of source %v: %v", s.ID.URI, err)
}
}
if s.Subtitle != nil {
if err := (*s.Subtitle).Check(); err != nil {
return fmt.Errorf("subtitle element of source: %v", err)
if err := s.Subtitle.Check(); err != nil {
return fmt.Errorf("subtitle element of source %v: %v", s.ID.URI, err)
}
}
if s.Title != nil {
if err := (*s.Title).Check(); err != nil {
return fmt.Errorf("title element of source: %v", err)
if err := s.Title.Check(); err != nil {
return fmt.Errorf("title element of source %v: %v", s.ID.URI, err)
}
}
if s.Updated != nil {
if err := s.Updated.Check(); err != nil {
return fmt.Errorf("updated element of source: %v", err)
return fmt.Errorf("updated element of source %v: %v", s.ID.URI, 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)
}
for i, e := range s.Extensions {
if err := e.Check(); err != nil {
return fmt.Errorf("extension element %v of source %v: %v", i, s.ID.URI, err)
}
}

View File

@ -6,16 +6,17 @@ import (
)
type Text interface {
isText() bool
Check() error
IsText() bool
}
// NewText creates a new Text. It returns a Text and an error.
func NewText(textType, content string) (Text, error) {
switch textType {
case "text":
case "text", "":
return &PlainText{Type: textType, Text: content}, nil
case "html":
return &PlainText{Type: textType, Text: html.EscapeString(content)}, nil
return &PlainText{Type: textType, Text: html.UnescapeString(content)}, nil
case "xhtml":
return &XHTMLText{
Type: textType,
@ -24,8 +25,6 @@ func NewText(textType, content string) (Text, error) {
Content: content,
},
}, nil
case "":
return &PlainText{Type: "text", Text: content}, nil
default:
return nil, fmt.Errorf("%v is not a valid text type", textType)
}

View File

@ -17,8 +17,11 @@ type XHTMLText struct {
XHTMLDiv XHTMLDiv
}
func (x *XHTMLText) IsText() bool { return true }
// isText checks whether the XHTMLText is a Text. It returns a bool.
func (x *XHTMLText) isText() bool { return true }
// Check checks the XHTMLText for incompatibilities with RFC4287. It returns an
// error.
func (x *XHTMLText) Check() error {
if x.Type != "xhtml" {
return errors.New("type attribute of xhtml text must be xhtml")