25 Commits

Author SHA1 Message Date
9ab48787d4 Make comments more useful 2025-01-24 23:06:32 +01:00
be79a13d48 Generalize updating entry and feed with update method 2025-01-24 23:05:46 +01:00
089c573aed Simplify some functions 2025-01-24 22:59:20 +01:00
d8d0526a05 Make NewCommonAttributes public 2024-10-20 15:59:46 +02:00
8fc6a10b0d Just implement update logic in Methods where it is needed, since if there is no pointer I cannot hand it to any function or method 2024-10-20 15:54:49 +02:00
0b70a8ee10 Turn updateDateTime into *Date.Update 2024-10-20 15:47:10 +02:00
da5d955b2d Implement more sophisticated updateDateTime function 2024-10-20 15:37:03 +02:00
ee72e91593 Drop unnecessary SetLabel method 2024-10-20 15:17:53 +02:00
ddf5b26a1e Quick fix 2024-10-20 15:15:14 +02:00
cf131f0bcf Touched up README 2024-10-20 15:10:58 +02:00
ae24db0c08 Clarified Add method's comments 2024-10-20 14:31:54 +02:00
8ce7d54d00 Return the index of the added element 2024-10-20 12:57:24 +02:00
764b143ff8 Changed comments a bit 2024-10-20 12:41:09 +02:00
bcf2532372 Rename id in Delete methods to index 2024-10-20 12:35:26 +02:00
b6b8970810 Add comments to generic functions 2024-10-20 12:30:23 +02:00
c0f5306715 Use pointers to make generic functions work 2024-10-20 12:20:25 +02:00
8a00759c4b Delete useless lines from README 2024-10-20 12:06:09 +02:00
a49e853efb Use generics for Add and Delete Methods 2024-10-20 12:03:26 +02:00
e0384904b4 Added Delete methods for slice elements to common attributes, entry, feed, person and source 2024-10-20 10:49:29 +02:00
e2986e70b1 Created all necessary Add methods for source 2024-10-20 10:42:12 +02:00
4c38753ff7 Change readme accordingly 2024-10-19 23:44:36 +02:00
7f30fd5411 Instanciate common attributes everywhere so one can simply use the extend method 2024-10-19 14:52:19 +02:00
a7a6b5c711 Get rid of unused functions 2024-10-19 14:28:03 +02:00
57db4178d0 Get rid of checks when creating constructs. Check should handle this. 2024-10-19 14:12:51 +02:00
960889f9e7 Bug fix 2024-10-19 12:37:44 +02:00
24 changed files with 696 additions and 557 deletions

278
README.md
View File

@ -2,7 +2,7 @@
An extensible Atom feed generator library that aims to be very close to RFC4287. An extensible Atom feed generator library that aims to be very close to RFC4287.
It diligently checks for compliance with the standard and provides functions for It diligently checks for compliance with the standard and provides functions for
easy creation and extension of elements. easy creation, extension and deletion of elements.
## Installation ## Installation
@ -14,124 +14,228 @@ go get git.streifling.com/jason/atom@latest
## Usage ## Usage
This library provides convenient functions to safely create and extend elements ### Basic Feed
and attributes of an Atom feed. This is because it can be hard to know all
pitfalls of RFC4287. The intended way of using atom is with these functions. This library provides convenient functions to safely create, extend and delete
elements and attributes of Atom feeds. It also provides checks for all
constructs' adherence to RFC4287.
```go ```go
package main package main
import ( import (
"fmt" "fmt"
"log" "log"
"git.streifling.com/jason/atom" "git.streifling.com/jason/atom"
) )
func main() { func main() {
feed, err := atom.NewFeed("Example Feed") feed := atom.NewFeed("Example Feed")
if err != nil { if err := feed.Check(); err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
author := atom.NewPerson("John Doe") author := atom.NewPerson("John Doe")
author.Email = "john.doe@example.com" author.Email = "john.doe@example.com"
feed.AddAuthor(author) if err := author.Check(); err != nil {
log.Fatalln(err)
}
feed.AddAuthor(author)
entry, err := atom.NewEntry("First Entry") entry := atom.NewEntry("First Entry")
if err != nil { entry.Content = atom.NewContent(atom.InlineText, "text", "This is the content of the first entry.")
log.Fatalln(err) if err := entry.Check(); err != nil {
} log.Fatalln(err)
content, err := atom.NewContent(atom.InlineText, "text", "This is the content of the first entry.") }
if err != nil { feed.AddEntry(entry)
log.Fatalln(err)
}
entry.Content = content
feed.AddEntry(entry)
if err := feed.Check(); err != nil { feedString, err := feed.ToXML("UTF-8")
log.Fatalln(err) if err != nil {
} log.Fatalln(err)
}
feedString, err := feed.ToXML("utf-8") fmt.Println(feedString)
if err != nil {
log.Fatalln(err)
}
fmt.Println(feedString)
} }
``` ```
It is also possible to use this library in a way similar to what other libraries It is also possible to use this library in a way similar to what other libraries
would provide. This is, of course, making it easier to make mistakes. provide.
```go ```go
package main package main
import ( import (
"fmt" "fmt"
"log" "log"
"time" "time"
"git.streifling.com/jason/atom" "git.streifling.com/jason/atom"
"github.com/google/uuid"
) )
func main() { func main() {
now := time.Now() now := time.Now()
feed := &atom.Feed{ feed := &atom.Feed{
Title: &atom.PlainText{ Title: &atom.PlainText{
Type: "text", Type: "text",
Text: "Example Feed", Text: "Example Feed",
}, },
ID: &atom.ID{URI: "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6"}, ID: &atom.ID{URI: fmt.Sprint("urn:uuid:", uuid.New())},
Updated: &atom.Date{DateTime: atom.DateTime(now)}, Updated: &atom.Date{DateTime: atom.DateTime(now)},
Authors: []*atom.Person{ Authors: []*atom.Person{
{ {
Name: "John Doe", Name: "John Doe",
Email: "john.doe@example.com", Email: "john.doe@example.com",
}, },
}, },
Entries: []*atom.Entry{ Entries: []*atom.Entry{
{ {
Title: &atom.PlainText{ Title: &atom.PlainText{
Type: "text", Type: "text",
Text: "First Entry", Text: "First Entry",
}, },
ID: &atom.ID{URI: "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a"}, ID: &atom.ID{URI: fmt.Sprint("urn:uuid:", uuid.New())},
Updated: &atom.Date{DateTime: atom.DateTime(now)}, Updated: &atom.Date{DateTime: atom.DateTime(now)},
Content: &atom.InlineTextContent{ Content: &atom.InlineTextContent{
Type: "text", Type: "text",
Text: "This is the content of the first entry.", Text: "This is the content of the first entry.",
}, },
}, },
}, },
} }
feedString, err := feed.ToXML("utf-8") feedString, err := feed.ToXML("UTF-8")
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
fmt.Println(feedString) fmt.Println(feedString)
} }
``` ```
The output of both ways of using it is an RFC4287 compliant Atom feed: The output of both ways of using it is an RFC4287 compliant Atom feed.
```xml ```xml
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"> <feed xmlns="http://www.w3.org/2005/Atom">
<author> <author>
<name>John Doe</name> <name>John Doe</name>
<email>john.doe@example.com</email> <email>john.doe@example.com</email>
</author> </author>
<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id> <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
<title type="text">Example Feed</title> <title type="text">Example Feed</title>
<updated>2024-10-18T05:49:08+02:00</updated> <updated>2024-10-18T05:49:08+02:00</updated>
<entry> <entry>
<content type="text">This is the content of the first entry.</content> <content type="text">This is the content of the first entry.</content>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id> <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
<title type="text">First Entry</title> <title type="text">First Entry</title>
<updated>2006-01-02T15:04:05+07:00</updated> <updated>2006-01-02T15:04:05+07:00</updated>
</entry> </entry>
</feed>
```
### Compliance and Error Checking
All Atom constructs have their own ```construct.Check()``` method. It checks for
compliance with RFC4287 and errors. This allows one to be certain that only
compliant constructs are added to their respective parent construct. It also
means that not the entire feed has to be checked every time a new construct is
added. Instead one only checks the current construct and all of its
sub-constructs with a single ```construct.Check()``` call.
```go
package main
import (
"fmt"
"log"
"git.streifling.com/jason/atom"
)
func main() {
feed := atom.NewFeed("Example Feed")
if err := feed.Check(); err != nil {
log.Fatalln(err)
}
entry := atom.NewEntry("One")
entry.Content = atom.NewContent(atom.InlineText, "text", "Entry One")
entry.AddAuthor(atom.NewPerson("John Doe"))
if err := entry.Check(); err != nil {
log.Fatalln(err)
}
feed.AddEntry(entry)
feedString, err := feed.ToXML("UTF-8")
if err != nil {
log.Fatalln(err)
}
fmt.Println(feedString)
}
```
### Adding and Deleting Items
To add elements to any slice one calls the appropriate
```someone.AddSomething(toBeAdded)``` method. It returns the item's index
number. To delete the item one calls the Delete method.
```go
package main
import (
"fmt"
"log"
"git.streifling.com/jason/atom"
)
func main() {
feed := atom.NewFeed("Feed")
if err := feed.Check(); err != nil {
log.Fatalln(err)
}
e1 := atom.NewEntry("One")
e1.Content = atom.NewContent(atom.InlineText, "text", "Entry One")
if err := e1.Check(); err != nil {
log.Fatalln(err)
}
i1 := feed.AddEntry(e1)
e2 := atom.NewEntry("Two")
e2.Content = atom.NewContent(atom.InlineText, "text", "Entry Two")
if err := e2.Check(); err != nil {
log.Fatalln(err)
}
feed.AddEntry(e2)
if err := feed.DeleteEntry(i1); err != nil {
log.Fatalln(err)
}
feedString, err := feed.ToXML("UTF-8")
if err != nil {
log.Fatalln(err)
}
fmt.Println(feedString)
}
```
The output of this example looks like this. It only shows the second entry.
```xml
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>urn:uuid:ae89d343-b535-447d-ac14-4b80d3e02a2f</id>
<title type="text">Example Feed</title>
<updated>2024-10-20T14:57:02+02:00</updated>
<entry>
<content type="text">Entry Two</content>
<id>urn:uuid:620c6f73-ee1d-4c1e-be98-b0b1ad7a053f</id>
<title type="text">Two</title>
<updated>2024-10-20T14:57:02+02:00</updated>
</entry>
</feed> </feed>
``` ```

61
atom.go
View File

@ -1,7 +1,9 @@
package atom package atom
import ( import (
"encoding/xml"
"fmt" "fmt"
"html"
"mime" "mime"
"regexp" "regexp"
"strings" "strings"
@ -10,6 +12,33 @@ import (
"golang.org/x/text/language" "golang.org/x/text/language"
) )
type Countable interface {
*xml.Attr | *Person | *Category | *Link | *ExtensionElement | *Entry
}
// addToSlice adds a Countable countable to to a *[]Countable slice. It returns
// the index as an int.
func addToSlice[C Countable](slice *[]C, countable C) int {
if *slice == nil {
*slice = make([]C, 0)
}
*slice = append(*slice, countable)
return len(*slice) - 1
}
// deleteFromSlice deletes the Countable at index from the *[]Countable slice.
// It returns an error.
func deleteFromSlice[C Countable](slice *[]C, index int) error {
length := len(*slice)
if index > length {
return fmt.Errorf("id %v out of range %v", index, length)
}
*slice = append((*slice)[:index], (*slice)[index+1:]...)
return nil
}
// isValidIRI checks whether an IRI is valid or not. It returns a bool. // isValidIRI checks whether an IRI is valid or not. It returns a bool.
// https://www.w3.org/2011/04/XMLSchema/TypeLibrary-IRI-RFC3987.xsd // https://www.w3.org/2011/04/XMLSchema/TypeLibrary-IRI-RFC3987.xsd
func isValidIRI(iri string) bool { func isValidIRI(iri string) bool {
@ -17,7 +46,7 @@ func isValidIRI(iri string) bool {
return regexp.MustCompile(pattern).MatchString(iri) return regexp.MustCompile(pattern).MatchString(iri)
} }
// isCorrectlyEscaped checks whether a string is correctly escaped as per // isCorrectlyEscaped checks whether the text is correctly escaped as per
// RFC4287. It returns a bool. // RFC4287. It returns a bool.
func isCorrectlyEscaped(text string) bool { func isCorrectlyEscaped(text string) bool {
relevantEntities := []string{"&amp;", "&lt;", "&gt;", "&quot;", "&apos;"} relevantEntities := []string{"&amp;", "&lt;", "&gt;", "&quot;", "&apos;"}
@ -31,8 +60,8 @@ func isCorrectlyEscaped(text string) bool {
return true return true
} }
// isCompositeMediaType checks whether a string is a composite media type. It // isCompositeMediaType checks whether the string m is a composite media type.
// returns a bool. // It returns a bool.
func isCompositeMediaType(m string) bool { func isCompositeMediaType(m string) bool {
mediaType, _, err := mime.ParseMediaType(m) mediaType, _, err := mime.ParseMediaType(m)
if err != nil { if err != nil {
@ -42,7 +71,7 @@ func isCompositeMediaType(m string) bool {
return strings.HasPrefix(mediaType, "multipart/") || strings.HasPrefix(mediaType, "message/") return strings.HasPrefix(mediaType, "multipart/") || strings.HasPrefix(mediaType, "message/")
} }
// isXMLMediaType checks whether a string is an xml media type. It returns a // isXMLMediaType checks whether the string m is an xml media type. It returns a
// bool. // bool.
func isXMLMediaType(m string) bool { func isXMLMediaType(m string) bool {
mediaType, _, err := mime.ParseMediaType(m) mediaType, _, err := mime.ParseMediaType(m)
@ -53,8 +82,8 @@ func isXMLMediaType(m string) bool {
return strings.HasSuffix(mediaType, "/xml") || strings.HasSuffix(mediaType, "+xml") return strings.HasSuffix(mediaType, "/xml") || strings.HasSuffix(mediaType, "+xml")
} }
// isValidMediaType checks whether a string is a valid media type. It returns a // isValidMediaType checks whether the string m is a valid media type. It
// bool. // returns a bool.
func isValidMediaType(m string) bool { func isValidMediaType(m string) bool {
mediaType, _, err := mime.ParseMediaType(m) mediaType, _, err := mime.ParseMediaType(m)
if err != nil { if err != nil {
@ -62,26 +91,28 @@ func isValidMediaType(m string) bool {
} }
typeParts := strings.Split(mediaType, "/") typeParts := strings.Split(mediaType, "/")
if len(typeParts) != 2 || typeParts[0] == "" || typeParts[1] == "" { return 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 the string languageTag is valid. It returns
// a bool.
func isValidLanguageTag(languageTag string) bool { func isValidLanguageTag(languageTag string) bool {
_, err := language.Parse(languageTag) _, err := language.Parse(languageTag)
return err == nil return err == nil
} }
// isValidAttribute checks whether an Attribute is valid. It returns a bool. // isValidAttribute checks whether the string attribute is valid. It returns a
// bool.
func isValidAttribute(attribute string) bool { func isValidAttribute(attribute string) bool {
regex := regexp.MustCompile(`^[a-zA-Z0-9_]+="[^"]*"$`) return regexp.MustCompile(`^[a-zA-Z0-9_]+="[^"]*"$`).MatchString(attribute)
return regex.MatchString(attribute)
} }
// NewURN generates an new valid IRI based on a UUID. It returns an IRI. // NewURN generates an new valid IRI based on a UUID. It returns an IRI.
func NewURN() string { func NewURN() string {
return fmt.Sprint("urn:uuid:", uuid.New()) return fmt.Sprint("urn:uuid:", uuid.New())
} }
// Unescape unescapes the string s. It returns an IRI.
func Unescape(s string) string {
return html.UnescapeString(s)
}

View File

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

View File

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

View File

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

View File

@ -10,15 +10,18 @@ type Date struct {
DateTime string `xml:",chardata"` DateTime string `xml:",chardata"`
} }
// DateTime formats a time.Time to string formated as defined by RFC3339. It // DateTime formats the time.Time t to a string as defined by RFC3339. It
// returns a string. // returns a string.
func DateTime(t time.Time) string { func DateTime(t time.Time) string {
return t.Format(time.RFC3339) return t.Format(time.RFC3339)
} }
// NewDate creates a new Date. It returns a *Date. // NewDate creates a new Date. It takes in a time.Time t and returns a *Date.
func NewDate(t time.Time) *Date { func NewDate(t time.Time) *Date {
return &Date{DateTime: DateTime(t)} return &Date{
CommonAttributes: NewCommonAttributes(),
DateTime: DateTime(t),
}
} }
// Check checks the Date for incompatibilities with RFC4287. It returns an // Check checks the Date for incompatibilities with RFC4287. It returns an

206
entry.go
View File

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

View File

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

237
feed.go
View File

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

View File

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

11
icon.go
View File

@ -12,13 +12,12 @@ type Icon struct {
URI string `xml:",chardata"` // IRI URI string `xml:",chardata"` // IRI
} }
// NewIcon creates a new Icon. It returns a *Icon and an error. // NewIcon creates a new Icon. It takes in a string uri and returns a *Icon.
func NewIcon(uri string) (*Icon, error) { func NewIcon(uri string) *Icon {
if !isValidIRI(uri) { return &Icon{
return nil, fmt.Errorf("uri %v not correctly formatted", uri) CommonAttributes: NewCommonAttributes(),
URI: 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

11
id.go
View File

@ -12,13 +12,12 @@ type ID struct {
URI string `xml:",chardata"` // IRI URI string `xml:",chardata"` // IRI
} }
// NewID creates a new ID. It returns a *ID and an error. // NewID creates a new ID. It takes in a string uri and returns a *ID.
func NewID(uri string) (*ID, error) { func NewID(uri string) *ID {
if !isValidIRI(uri) { return &ID{
return nil, fmt.Errorf("uri %v not correctly formatted", uri) CommonAttributes: NewCommonAttributes(),
URI: uri,
} }
return &ID{URI: 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

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

View File

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

View File

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

29
link.go
View File

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

11
logo.go
View File

@ -11,13 +11,12 @@ type Logo struct {
URI string `xml:",chardata"` // IRI URI string `xml:",chardata"` // IRI
} }
// NewLogo creates a new Logo. It returns a *Logo and an error. // NewLogo creates a new Logo. It takes in a string uri and returns a *Logo.
func NewLogo(uri string) (*Logo, error) { func NewLogo(uri string) *Logo {
if !isValidIRI(uri) { return &Logo{
return nil, fmt.Errorf("uri %v not correctly formatted", uri) CommonAttributes: NewCommonAttributes(),
URI: 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

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

View File

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

View File

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

View File

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

14
text.go
View File

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

View File

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

View File

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