Compare commits

..

No commits in common. "main" and "v0.5.0" have entirely different histories.
main ... v0.5.0

24 changed files with 250 additions and 403 deletions

269
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, extension and deletion of elements. easy creation and extension of elements.
## Installation ## Installation
@ -14,47 +14,45 @@ go get git.streifling.com/jason/atom@latest
## Usage ## Usage
### Basic Feed This library provides convenient functions to safely create and extend elements
and attributes of Atom feeds. It also provides checks for all constructs'
This library provides convenient functions to safely create, extend and delete adherence to RFC4287.
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 := atom.NewFeed("Example Feed") feed := atom.NewFeed("Example Feed")
if err := feed.Check(); 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"
if err := author.Check(); err != nil { if err := author.Check(); err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
feed.AddAuthor(author) feed.AddAuthor(author)
entry := atom.NewEntry("First Entry") entry := atom.NewEntry("First Entry")
entry.Content = atom.NewContent(atom.InlineText, "text", "This is the content of the first entry.") entry.Content = atom.NewContent(atom.InlineText, "text", "This is the content of the first entry.")
if err := entry.Check(); err != nil { if err := entry.Check(); err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
feed.AddEntry(entry) feed.AddEntry(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)
} }
``` ```
@ -65,177 +63,70 @@ provide.
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: fmt.Sprint("urn:uuid:", uuid.New())}, ID: &atom.ID{URI: "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6"},
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: fmt.Sprint("urn:uuid:", uuid.New())}, ID: &atom.ID{URI: "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a"},
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>
``` ```

40
atom.go
View File

@ -16,19 +16,16 @@ type Countable interface {
*xml.Attr | *Person | *Category | *Link | *ExtensionElement | *Entry *xml.Attr | *Person | *Category | *Link | *ExtensionElement | *Entry
} }
// addToSlice adds a Countable countable to to a *[]Countable slice. It returns // addToSlice adds a Countable to to a *[]Countable
// the index as an int. func addToSlice[C Countable](slice *[]C, countable C) {
func addToSlice[C Countable](slice *[]C, countable C) int {
if *slice == nil { if *slice == nil {
*slice = make([]C, 0) *slice = make([]C, 0)
} }
*slice = append(*slice, countable) *slice = append(*slice, countable)
return len(*slice) - 1
} }
// deleteFromSlice deletes the Countable at index from the *[]Countable slice. // deleteFromSlice deletes the Countable with the index from the *[]Countable.
// It returns an error. // It return an error.
func deleteFromSlice[C Countable](slice *[]C, index int) error { func deleteFromSlice[C Countable](slice *[]C, index int) error {
length := len(*slice) length := len(*slice)
if index > length { if index > length {
@ -46,7 +43,7 @@ func isValidIRI(iri string) bool {
return regexp.MustCompile(pattern).MatchString(iri) return regexp.MustCompile(pattern).MatchString(iri)
} }
// isCorrectlyEscaped checks whether the text is correctly escaped as per // isCorrectlyEscaped checks whether a string 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;"}
@ -60,8 +57,8 @@ func isCorrectlyEscaped(text string) bool {
return true return true
} }
// isCompositeMediaType checks whether the string m is a composite media type. // isCompositeMediaType checks whether a string is a composite media type. It
// It returns a bool. // 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 {
@ -71,7 +68,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 the string m is an xml media type. It returns a // isXMLMediaType checks whether a string 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)
@ -82,8 +79,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 the string m is a valid media type. It // isValidMediaType checks whether a string is a valid media type. It returns a
// returns a bool. // 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 {
@ -91,20 +88,23 @@ func isValidMediaType(m string) bool {
} }
typeParts := strings.Split(mediaType, "/") typeParts := strings.Split(mediaType, "/")
return len(typeParts) == 2 && typeParts[0] != "" && typeParts[1] != "" if len(typeParts) != 2 || typeParts[0] == "" || typeParts[1] == "" {
return false
}
return true
} }
// isValidLanguageTag checks whether the string languageTag is valid. It returns // isValidLanguageTag checks whether a LanguageTag is valid. It returns a bool.
// 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 the string attribute is valid. It returns a // isValidAttribute checks whether an Attribute is valid. It returns a bool.
// bool.
func isValidAttribute(attribute string) bool { func isValidAttribute(attribute string) bool {
return regexp.MustCompile(`^[a-zA-Z0-9_]+="[^"]*"$`).MatchString(attribute) regex := regexp.MustCompile(`^[a-zA-Z0-9_]+="[^"]*"$`)
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.
@ -112,7 +112,7 @@ 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. // Unescape unescapes a string. It returns an IRI.
func Unescape(s string) string { func Unescape(s string) string {
return html.UnescapeString(s) return html.UnescapeString(s)
} }

View File

@ -10,18 +10,22 @@ 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"` // Must be unescaped Label string `xml:"label,attr,omitempty"`
} }
// NewCategory creates a new Category. It takes in a string term and returns a // NewCategory creates a new Category. It returns a *Category.
// *Category.
func NewCategory(term string) *Category { func NewCategory(term string) *Category {
return &Category{ return &Category{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
Term: term, Term: term,
} }
} }
// SetLabel sets the Label attribute of the Category.
func (c *Category) SetLabel(label string) {
c.Label = Unescape(label)
}
// Check checks the Category for incompatibilities with RFC4287. It returns an // Check checks the Category for incompatibilities with RFC4287. It returns an
// error. // error.
func (c *Category) Check() error { func (c *Category) Check() error {

View File

@ -13,18 +13,17 @@ type CommonAttributes struct {
// NewCommonAttributes creates a new set of CommonAttributes. It returns a // NewCommonAttributes creates a new set of CommonAttributes. It returns a
// *CommonAttributes. // *CommonAttributes.
func NewCommonAttributes() *CommonAttributes { func newCommonAttributes() *CommonAttributes {
return new(CommonAttributes) return new(CommonAttributes)
} }
// AddAttribute adds an attribute to the CommonAttributes. It takes in the // AddAttribute adds the attribute to the CommonAttributes.
// strings name and value and returns the index as an int. func (c *CommonAttributes) AddAttribute(name, value string) {
func (c *CommonAttributes) AddAttribute(name, value string) int { addToSlice(&c.UndefinedAttributes, &xml.Attr{Name: xml.Name{Local: name}, Value: value})
return addToSlice(&c.UndefinedAttributes, &xml.Attr{Name: xml.Name{Local: name}, Value: value})
} }
// DeleteAttribute deletes the attribute at index from the CommonAttributes. It // DeleteAttribute deletes the attribute at index from the CommonAttributes. It
// returns an error. // return an error.
func (c *CommonAttributes) DeleteAttribute(index int) error { func (c *CommonAttributes) DeleteAttribute(index int) error {
if err := deleteFromSlice(&c.UndefinedAttributes, index); err != nil { if err := deleteFromSlice(&c.UndefinedAttributes, index); err != nil {
return fmt.Errorf("error deleting undefined attribute %v from common attributes %v: %v", index, c, err) return fmt.Errorf("error deleting undefined attribute %v from common attributes %v: %v", index, c, err)

View File

@ -14,10 +14,7 @@ type Content interface {
Check() error Check() error
} }
// NewContent creates a new Content. It takes in an int contentType, a string // NewContent creates a new Content. It returns a Content and an 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 { func NewContent(contentType int, mediaType string, content any) Content {
switch contentType { switch contentType {
case 0: case 0:

View File

@ -10,16 +10,16 @@ type Date struct {
DateTime string `xml:",chardata"` DateTime string `xml:",chardata"`
} }
// DateTime formats the time.Time t to a string as defined by RFC3339. It // DateTime formats a time.Time to string formated 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 takes in a time.Time t and returns a *Date. // NewDate creates a new Date. It returns a *Date.
func NewDate(t time.Time) *Date { func NewDate(t time.Time) *Date {
return &Date{ return &Date{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
DateTime: DateTime(t), DateTime: DateTime(t),
} }
} }

View File

@ -30,15 +30,14 @@ type Entry struct {
} }
// checkAuthors checks the entry's authors for incompatibilities with RFC4287. // checkAuthors checks the entry's authors for incompatibilities with RFC4287.
// It takes in a bool authorIsInFeed and returns an errors. // It 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(authorIsInFeed bool) error { func (e *Entry) checkAuthors(authorInFeed bool) error {
if e.Authors == nil { if e.Authors == nil {
if !authorIsInFeed { if !authorInFeed {
if e.Source == nil { if e.Source == nil {
return fmt.Errorf("no authors set in entry %v", e.ID.URI) return fmt.Errorf("no authors set in entry %v", e.ID.URI)
} }
@ -57,109 +56,96 @@ func (e *Entry) checkAuthors(authorIsInFeed bool) error {
return nil return nil
} }
// update sets the Updated time to time.Now. // NewEntry creates a new Entry. It returns a *Entry.
func (e *Entry) update() {
if e.Updated == nil {
e.Updated = NewDate(time.Now())
} else {
e.Updated.DateTime = DateTime(time.Now())
}
}
// NewEntry creates a new Entry. It takes in a string title and returns a
// *Entry.
func NewEntry(title string) *Entry { func NewEntry(title string) *Entry {
return &Entry{ return &Entry{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
ID: NewID(NewURN()), ID: NewID(NewURN()),
Title: NewText("text", title), Title: NewText("text", title),
Updated: NewDate(time.Now()), Updated: NewDate(time.Now()),
} }
} }
// AddAuthor adds the Person a as an author to the Entry. It returns the index // AddAuthor adds the Person as an author to the Entry.
// as an int. func (e *Entry) AddAuthor(p *Person) {
func (e *Entry) AddAuthor(a *Person) int { addToSlice(&e.Authors, p)
e.update() e.Updated = NewDate(time.Now())
return addToSlice(&e.Authors, a)
} }
// DeleteAuthor deletes the Person at index from the Entry. It returns an error. // DeleteAuthor deletes the Person at index from the Entry. It return an error.
func (e *Entry) DeleteAuthor(index int) error { func (e *Entry) DeleteAuthor(index int) error {
if err := deleteFromSlice(&e.Authors, index); err != nil { if err := deleteFromSlice(&e.Authors, index); err != nil {
return fmt.Errorf("error deleting author %v from entry %v: %v", index, e.ID.URI, err) return fmt.Errorf("error deleting author %v from entry %v: %v", index, e.ID.URI, err)
} }
e.update() e.Updated = NewDate(time.Now())
return nil return nil
} }
// AddCategory adds the Category c to the Entry. It returns the index as an int. // AddCategory adds the Category to the Entry.
func (e *Entry) AddCategory(c *Category) int { func (e *Entry) AddCategory(c *Category) {
e.update() addToSlice(&e.Categories, c)
return addToSlice(&e.Categories, c) e.Updated = NewDate(time.Now())
} }
// DeleteCategory deletes the Category at index from the Entry. It returns an // DeleteCategory deletes the Category at index from the Entry. It return an
// error. // error.
func (e *Entry) DeleteCategory(index int) error { func (e *Entry) DeleteCategory(index int) error {
if err := deleteFromSlice(&e.Categories, index); err != nil { if err := deleteFromSlice(&e.Categories, index); err != nil {
return fmt.Errorf("error deleting category %v from entry %v: %v", index, e.ID.URI, err) return fmt.Errorf("error deleting category %v from entry %v: %v", index, e.ID.URI, err)
} }
e.update() e.Updated = NewDate(time.Now())
return nil return nil
} }
// AddContributor adds the Person c as a contributor to the Entry. It returns // AddContributor adds the Person as a contributor to the Entry.
// the index as an int. func (e *Entry) AddContributor(c *Person) {
func (e *Entry) AddContributor(c *Person) int { addToSlice(&e.Contributors, c)
e.update() e.Updated = NewDate(time.Now())
return addToSlice(&e.Contributors, c)
} }
// DeleteContributor deletes the Person at index from the Entry. It returns an // DeleteContributor deletes the Person at index from the Entry. It return an
// error. // error.
func (e *Entry) DeleteContributor(index int) error { func (e *Entry) DeleteContributor(index int) error {
if err := deleteFromSlice(&e.Contributors, index); err != nil { if err := deleteFromSlice(&e.Contributors, index); err != nil {
return fmt.Errorf("error deleting contributor %v from entry %v: %v", index, e.ID.URI, err) return fmt.Errorf("error deleting contributor %v from entry %v: %v", index, e.ID.URI, err)
} }
e.update() e.Updated = NewDate(time.Now())
return nil return nil
} }
// AddLink adds the Link l to the Entry. It returns the index as an int. // AddLink adds the Link to the Entry.
func (e *Entry) AddLink(l *Link) int { func (e *Entry) AddLink(l *Link) {
e.update() addToSlice(&e.Links, l)
return addToSlice(&e.Links, l) e.Updated = NewDate(time.Now())
} }
// DeleteLink deletes the Link at index from the Entry. It returns an error. // DeleteLink deletes the Link at index from the Entry. It return an error.
func (e *Entry) DeleteLink(index int) error { func (e *Entry) DeleteLink(index int) error {
if err := deleteFromSlice(&e.Links, index); err != nil { if err := deleteFromSlice(&e.Links, index); err != nil {
return fmt.Errorf("error deleting link %v from entry %v: %v", index, e.ID.URI, err) return fmt.Errorf("error deleting link %v from entry %v: %v", index, e.ID.URI, err)
} }
e.update() e.Updated = NewDate(time.Now())
return nil return nil
} }
// AddExtension adds the ExtensionElement x to the Entry. It returns the index // AddExtension adds the ExtensionElement to the Entry.
// as an int. func (e *Entry) AddExtension(x *ExtensionElement) {
func (e *Entry) AddExtension(x *ExtensionElement) int { addToSlice(&e.Extensions, x)
e.update() e.Updated = NewDate(time.Now())
return addToSlice(&e.Extensions, x)
} }
// DeleteExtension deletes the Extension at index from the Entry. It returns an // DeleteExtension deletes the Extension at index from the Entry. It return an
// error. // error.
func (e *Entry) DeleteExtension(index int) error { func (e *Entry) DeleteExtension(index int) error {
if err := deleteFromSlice(&e.Extensions, index); err != nil { if err := deleteFromSlice(&e.Extensions, index); err != nil {
return fmt.Errorf("error deleting extension %v from entry %v: %v", index, e.ID.URI, err) return fmt.Errorf("error deleting extension %v from entry %v: %v", index, e.ID.URI, err)
} }
e.update() e.Updated = NewDate(time.Now())
return nil return nil
} }
@ -277,8 +263,7 @@ func (e *Entry) Check() error {
return nil return nil
} }
// ToXML converts the Feed to XML. It takes in a string encoding and returns a // ToXML converts the Feed to XML. It returns a string and an error.
// 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 {

View File

@ -10,8 +10,8 @@ type ExtensionElement struct {
XMLName xml.Name XMLName xml.Name
} }
// NewExtensionElement creates a new ExtensionElement. It takes in a string name // NewExtensionElement creates a new ExtensionElement. It returns a
// and any value and returns a *ExtensionElement. // *ExtensionElement.
func NewExtensionElement(name string, value any) *ExtensionElement { func NewExtensionElement(name string, value any) *ExtensionElement {
return &ExtensionElement{XMLName: xml.Name{Local: name}, Value: value} return &ExtensionElement{XMLName: xml.Name{Local: name}, Value: value}
} }

96
feed.go
View File

@ -25,131 +25,116 @@ type Feed struct {
Entries []*Entry `xml:",omitempty"` Entries []*Entry `xml:",omitempty"`
} }
// update sets the Updated time to time.Now. // NewFeed creates a new Feed. It returns a *Feed.
func (f *Feed) update() {
if f.Updated == nil {
f.Updated = NewDate(time.Now())
} else {
f.Updated.DateTime = DateTime(time.Now())
}
}
// NewFeed creates a new Feed. It takes in a string title and returns a *Feed.
func NewFeed(title string) *Feed { func NewFeed(title string) *Feed {
return &Feed{ return &Feed{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
ID: NewID(NewURN()), ID: NewID(NewURN()),
Title: NewText("text", title), Title: NewText("text", title),
Updated: NewDate(time.Now()), Updated: NewDate(time.Now()),
} }
} }
// AddAuthor adds the Person a as an author to the Feed. It returns the index as // AddAuthor adds the Person as an author to the Feed.
// an int. func (f *Feed) AddAuthor(p *Person) {
func (f *Feed) AddAuthor(a *Person) int { addToSlice(&f.Authors, p)
f.update() f.Updated = NewDate(time.Now())
return addToSlice(&f.Authors, a)
} }
// DeleteAuthor deletes the Person at index from the Feed. It returns an error. // DeleteAuthor deletes the Person at index from the Feed. It return an error.
func (f *Feed) DeleteAuthor(index int) error { func (f *Feed) DeleteAuthor(index int) error {
if err := deleteFromSlice(&f.Authors, index); err != nil { if err := deleteFromSlice(&f.Authors, index); err != nil {
return fmt.Errorf("error deleting author %v from entry %v: %v", index, f.ID.URI, err) return fmt.Errorf("error deleting author %v from entry %v: %v", index, f.ID.URI, err)
} }
f.update() f.Updated = NewDate(time.Now())
return nil return nil
} }
// AddCategory adds the Category c to the Feed. It returns the index as an int. // AddCategory adds the Category to the Feed.
func (f *Feed) AddCategory(c *Category) int { func (f *Feed) AddCategory(c *Category) {
f.update() addToSlice(&f.Categories, c)
return addToSlice(&f.Categories, c) f.Updated = NewDate(time.Now())
} }
// DeleteCategory deletes the Category at index from the Feed. It returns an // DeleteCategory deletes the Category at index from the Feed. It return an
// error. // error.
func (f *Feed) DeleteCategory(index int) error { func (f *Feed) DeleteCategory(index int) error {
if err := deleteFromSlice(&f.Categories, index); err != nil { if err := deleteFromSlice(&f.Categories, index); err != nil {
return fmt.Errorf("error deleting category %v from entry %v: %v", index, f.ID.URI, err) return fmt.Errorf("error deleting category %v from entry %v: %v", index, f.ID.URI, err)
} }
f.update() f.Updated = NewDate(time.Now())
return nil return nil
} }
// AddContributor adds the Person c as a contributor to the Feed. It returns the // AddContributor adds the Person as a contributor to the Feed.
// index as an int. func (f *Feed) AddContributor(c *Person) {
func (f *Feed) AddContributor(c *Person) int { addToSlice(&f.Contributors, c)
f.update() f.Updated = NewDate(time.Now())
return addToSlice(&f.Contributors, c)
} }
// DeleteContributor deletes the Person at index from the Feed. It returns an // DeleteContributor deletes the Person at index from the Feed. It return an
// error. // error.
func (f *Feed) DeleteContributor(index int) error { func (f *Feed) DeleteContributor(index int) error {
if err := deleteFromSlice(&f.Contributors, index); err != nil { if err := deleteFromSlice(&f.Contributors, index); err != nil {
return fmt.Errorf("error deleting contributor %v from entry %v: %v", index, f.ID.URI, err) return fmt.Errorf("error deleting contributor %v from entry %v: %v", index, f.ID.URI, err)
} }
f.update() f.Updated = NewDate(time.Now())
return nil return nil
} }
// AddLink adds the Link l to the Feed. It returns the index as an int. // AddLink adds the Link to the Feed. There should be one Link with Rel "self".
// func (f *Feed) AddLink(l *Link) {
// There should be one Link with Rel "self". addToSlice(&f.Links, l)
func (f *Feed) AddLink(l *Link) int { f.Updated = NewDate(time.Now())
f.update()
return addToSlice(&f.Links, l)
} }
// DeleteLink deletes the Link at index from the Feed. It returns an error. // DeleteLink deletes the Link at index from the Feed. It return an error.
func (f *Feed) DeleteLink(index int) error { func (f *Feed) DeleteLink(index int) error {
if err := deleteFromSlice(&f.Links, index); err != nil { if err := deleteFromSlice(&f.Links, index); err != nil {
return fmt.Errorf("error deleting link %v from entry %v: %v", index, f.ID.URI, err) return fmt.Errorf("error deleting link %v from entry %v: %v", index, f.ID.URI, err)
} }
f.update() f.Updated = NewDate(time.Now())
return nil return nil
} }
// AddExtension adds the Extension e to the Feed. It returns the index as an // AddExtension adds the Extension to the Feed.
// int. func (f *Feed) AddExtension(e *ExtensionElement) {
func (f *Feed) AddExtension(e *ExtensionElement) int { addToSlice(&f.Extensions, e)
f.update() f.Updated = NewDate(time.Now())
return addToSlice(&f.Extensions, e)
} }
// DeleteExtension deletes the Extension at index from the Feed. It returns an // DeleteExtension deletes the Extension at index from the Feed. It return an
// error. // error.
func (f *Feed) DeleteExtension(index int) error { func (f *Feed) DeleteExtension(index int) error {
if err := deleteFromSlice(&f.Extensions, index); err != nil { if err := deleteFromSlice(&f.Extensions, index); err != nil {
return fmt.Errorf("error deleting extension %v from entry %v: %v", index, f.ID.URI, err) return fmt.Errorf("error deleting extension %v from entry %v: %v", index, f.ID.URI, err)
} }
f.update() f.Updated = NewDate(time.Now())
return nil return nil
} }
// AddEntry adds the Entry e to the Feed. It returns the index as an int. // AddEntry adds the Entry to the Feed.
func (f *Feed) AddEntry(e *Entry) int { func (f *Feed) AddEntry(e *Entry) {
f.update() addToSlice(&f.Entries, e)
return addToSlice(&f.Entries, e) f.Updated = NewDate(time.Now())
} }
// DeleteEntry deletes the Entry at index from the Feed. It returns an error. // DeleteEntry deletes the Entry at index from the Feed. It return an error.
func (f *Feed) DeleteEntry(index int) error { func (f *Feed) DeleteEntry(index int) error {
if err := deleteFromSlice(&f.Entries, index); err != nil { if err := deleteFromSlice(&f.Entries, index); err != nil {
return fmt.Errorf("error deleting entry %v from entry %v: %v", index, f.ID.URI, err) return fmt.Errorf("error deleting entry %v from entry %v: %v", index, f.ID.URI, err)
} }
f.update() f.Updated = NewDate(time.Now())
return nil return nil
} }
// DeleteEntryByURI deletes the Entry from the Feed. It takes in a string uri // DeleteEntryByURI deletes the Entry from the Feed. It return an error.
// and returns an error.
func (f *Feed) DeleteEntryByURI(uri string) error { func (f *Feed) DeleteEntryByURI(uri string) error {
if !isValidIRI(uri) { if !isValidIRI(uri) {
return fmt.Errorf("error deleting entry from feed %v: uri %v invalid", f.ID.URI, uri) return fmt.Errorf("error deleting entry from feed %v: uri %v invalid", f.ID.URI, uri)
@ -167,8 +152,7 @@ func (f *Feed) DeleteEntryByURI(uri string) error {
} }
f.Entries = append(f.Entries[:index], f.Entries[index+1:]...) f.Entries = append(f.Entries[:index], f.Entries[index+1:]...)
f.update() f.Updated = NewDate(time.Now())
return nil return nil
} }

View File

@ -14,11 +14,10 @@ type Generator struct {
Text string `xml:",chardata"` Text string `xml:",chardata"`
} }
// NewGenerator creates a new Generator. It takes in a string text and returns a // NewGenerator creates a new Generator. It returns a *Generator.
// *Generator.
func NewGenerator(text string) *Generator { func NewGenerator(text string) *Generator {
return &Generator{ return &Generator{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
Text: html.UnescapeString(text), Text: html.UnescapeString(text),
} }
} }

View File

@ -12,10 +12,10 @@ type Icon struct {
URI string `xml:",chardata"` // IRI URI string `xml:",chardata"` // IRI
} }
// NewIcon creates a new Icon. It takes in a string uri and returns a *Icon. // NewIcon creates a new Icon. It returns a *Icon.
func NewIcon(uri string) *Icon { func NewIcon(uri string) *Icon {
return &Icon{ return &Icon{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
URI: uri, URI: uri,
} }
} }

4
id.go
View File

@ -12,10 +12,10 @@ type ID struct {
URI string `xml:",chardata"` // IRI URI string `xml:",chardata"` // IRI
} }
// NewID creates a new ID. It takes in a string uri and returns a *ID. // NewID creates a new ID. It returns a *ID.
func NewID(uri string) *ID { func NewID(uri string) *ID {
return &ID{ return &ID{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
URI: uri, URI: uri,
} }
} }

View File

@ -13,13 +13,13 @@ type InlineOtherContent struct {
Type string `xml:"type,attr,omitempty"` // MediaType Type string `xml:"type,attr,omitempty"` // MediaType
} }
// newInlineOtherContent creates a new InlineOtherContent. It takes in the string // newInlineOtherContent creates a new InlineOtherContent. It returns a
// mediaType and any content and returns a *InlineOtherContent and an error. // *InlineOtherContent and an error.
func newInlineOtherContent(mediaType string, content any) *InlineOtherContent { func newInlineOtherContent(mediaType string, content any) *InlineOtherContent {
mediaType, _, _ = mime.ParseMediaType(mediaType) mediaType, _, _ = mime.ParseMediaType(mediaType)
return &InlineOtherContent{ return &InlineOtherContent{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
Type: mediaType, Type: mediaType,
AnyElement: content, AnyElement: content,
} }

View File

@ -12,11 +12,11 @@ type InlineTextContent struct {
Text string `xml:",chardata"` Text string `xml:",chardata"`
} }
// newInlineTextContent creates a new InlineTextContent. It takes in the strings // newInlineTextContent creates a new InlineTextContent. It returns a
// mediaType and text and returns a *InlineTextContent. // *InlineTextContent.
func newInlineTextContent(mediaType, text string) *InlineTextContent { func newInlineTextContent(mediaType, text string) *InlineTextContent {
return &InlineTextContent{ return &InlineTextContent{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
Type: mediaType, Type: mediaType,
Text: text, Text: text,
} }

View File

@ -12,11 +12,11 @@ type InlineXHTMLContent struct {
Type string `xml:"type,attr"` Type string `xml:"type,attr"`
} }
// newInlineXHTMLContent creates a new InlineXHTMLContent. It takes in the // newInlineXHTMLContent creates a new InlineXHTMLContent. It returns a
// string mediaType and the XHTMLDiv div and returns a *InlineXHTMLContent. // *InlineXHTMLContent.
func newInlineXHTMLContent(mediaType string, div *XHTMLDiv) *InlineXHTMLContent { func newInlineXHTMLContent(mediaType string, div *XHTMLDiv) *InlineXHTMLContent {
return &InlineXHTMLContent{ return &InlineXHTMLContent{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
Type: mediaType, Type: mediaType,
XHTMLDiv: div, XHTMLDiv: div,
} }

View File

@ -17,10 +17,10 @@ type Link struct {
Length uint `xml:"length,attr,omitempty"` Length uint `xml:"length,attr,omitempty"`
} }
// NewLink creates a new Link. It takes in the string href and returns a *Link. // NewLink creates a new Link. It returns a *Link.
func NewLink(href string) *Link { func NewLink(href string) *Link {
return &Link{ return &Link{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
Href: href, Href: href,
} }
} }

View File

@ -11,10 +11,10 @@ type Logo struct {
URI string `xml:",chardata"` // IRI URI string `xml:",chardata"` // IRI
} }
// NewLogo creates a new Logo. It takes in a string uri and returns a *Logo. // NewLogo creates a new Logo. It returns a *Logo.
func NewLogo(uri string) *Logo { func NewLogo(uri string) *Logo {
return &Logo{ return &Logo{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
URI: uri, URI: uri,
} }
} }

View File

@ -13,13 +13,13 @@ type OutOfLineContent struct {
SRC string `xml:"src,attr"` // IRI SRC string `xml:"src,attr"` // IRI
} }
// newOutOfLineContent creates a new OutOfLineContent. It takes in the strings // newOutOfLineContent creates a new OutOfLineContent. It returns a
// mediaType and src and returns a *OutOfLineContent. // *OutOfLineContent.
func newOutOfLineContent(mediaType, src string) *OutOfLineContent { func newOutOfLineContent(mediaType, src string) *OutOfLineContent {
mediaType, _, _ = mime.ParseMediaType(mediaType) mediaType, _, _ = mime.ParseMediaType(mediaType)
return &OutOfLineContent{ return &OutOfLineContent{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
Type: mediaType, Type: mediaType,
SRC: src, SRC: src,
} }

View File

@ -13,22 +13,20 @@ type Person struct {
Extensions []*ExtensionElement `xml:",any,omitempty"` Extensions []*ExtensionElement `xml:",any,omitempty"`
} }
// NewPerson creates a new Person. It takes in a string name and returns a // NewPerson creates a new Person. It returns a *Person.
// *Person.
func NewPerson(name string) *Person { func NewPerson(name string) *Person {
return &Person{ return &Person{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
Name: name, Name: name,
} }
} }
// AddExtension adds the Extension e to the Person. It returns the index as an // AddExtension adds the Extension to the Person.
// int. func (p *Person) AddExtension(e *ExtensionElement) {
func (p *Person) AddExtension(e *ExtensionElement) int { addToSlice(&p.Extensions, e)
return addToSlice(&p.Extensions, e)
} }
// DeleteExtension deletes the Extension at index from the Person. It returns an // DeleteExtension deletes the Extension at index from the Person. It return an
// error. // error.
func (p *Person) DeleteExtension(index int) error { func (p *Person) DeleteExtension(index int) error {
if err := deleteFromSlice(&p.Extensions, index); err != nil { if err := deleteFromSlice(&p.Extensions, index); err != nil {

View File

@ -13,11 +13,10 @@ 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 takes in the strings textType and // newPlainText creates a new PlainText. It returns a *PlainText.
// content and returns a *PlainText.
func newPlainText(textType, content string) *PlainText { func newPlainText(textType, content string) *PlainText {
return &PlainText{ return &PlainText{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
Type: textType, Type: textType,
Text: content, Text: content,
} }

View File

@ -25,17 +25,15 @@ 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 &Source{CommonAttributes: NewCommonAttributes()} return &Source{CommonAttributes: newCommonAttributes()}
} }
// AddAuthor adds the Person a as an author to the Source. It returns the index // AddAuthor adds the Person as an author to the Source.
// as an int. func (s *Source) AddAuthor(p *Person) {
func (s *Source) AddAuthor(a *Person) int { addToSlice(&s.Authors, p)
return addToSlice(&s.Authors, a)
} }
// DeleteAuthor deletes the Person at index from the Source. It returns an // DeleteAuthor deletes the Person at index from the Source. It return an error.
// error.
func (s *Source) DeleteAuthor(index int) error { func (s *Source) DeleteAuthor(index int) error {
if err := deleteFromSlice(&s.Authors, index); err != nil { if err := deleteFromSlice(&s.Authors, index); err != nil {
return fmt.Errorf("error deleting author %v from source %v: %v", index, s, err) return fmt.Errorf("error deleting author %v from source %v: %v", index, s, err)
@ -43,12 +41,12 @@ func (s *Source) DeleteAuthor(index int) error {
return nil return nil
} }
// AddCategory adds the Category c to the Source. It returns the index as an int. // AddCategory adds the Category to the Source.
func (s *Source) AddCategory(c *Category) int { func (s *Source) AddCategory(c *Category) {
return addToSlice(&s.Categories, c) addToSlice(&s.Categories, c)
} }
// DeleteCategory deletes the Category at index from the Source. It returns an // DeleteCategory deletes the Category at index from the Source. It return an
// error. // error.
func (s *Source) DeleteCategory(index int) error { func (s *Source) DeleteCategory(index int) error {
if err := deleteFromSlice(&s.Categories, index); err != nil { if err := deleteFromSlice(&s.Categories, index); err != nil {
@ -57,13 +55,12 @@ func (s *Source) DeleteCategory(index int) error {
return nil return nil
} }
// AddContributor adds the Person c as a contributor to the Source. It returns // AddContributor adds the Person as a contributor to the Source.
// the index as an int. func (s *Source) AddContributor(c *Person) {
func (s *Source) AddContributor(c *Person) int { addToSlice(&s.Contributors, c)
return addToSlice(&s.Contributors, c)
} }
// DeleteContributor deletes the Person at index from the Source. It returns an // DeleteContributor deletes the Person at index from the Source. It return an
// error. // error.
func (s *Source) DeleteContributor(index int) error { func (s *Source) DeleteContributor(index int) error {
if err := deleteFromSlice(&s.Contributors, index); err != nil { if err := deleteFromSlice(&s.Contributors, index); err != nil {
@ -72,12 +69,12 @@ func (s *Source) DeleteContributor(index int) error {
return nil return nil
} }
// AddLink adds the Link l to the Source. It returns the index as an int. // AddLink adds the Link to the Source.
func (s *Source) AddLink(l *Link) int { func (s *Source) AddLink(l *Link) {
return addToSlice(&s.Links, l) addToSlice(&s.Links, l)
} }
// DeleteLink deletes the Link at index from the Source. It returns an error. // DeleteLink deletes the Link at index from the Source. It return an error.
func (s *Source) DeleteLink(index int) error { func (s *Source) DeleteLink(index int) error {
if err := deleteFromSlice(&s.Links, index); err != nil { if err := deleteFromSlice(&s.Links, index); err != nil {
return fmt.Errorf("error deleting link %v from source %v: %v", index, s, err) return fmt.Errorf("error deleting link %v from source %v: %v", index, s, err)
@ -85,13 +82,12 @@ func (s *Source) DeleteLink(index int) error {
return nil return nil
} }
// AddExtension adds the ExtensionElement e to the Source. It returns the index // AddExtension adds the ExtensionElement to the Source.
// as an int. func (s *Source) AddExtension(e *ExtensionElement) {
func (s *Source) AddExtension(e *ExtensionElement) int { addToSlice(&s.Extensions, e)
return addToSlice(&s.Extensions, e)
} }
// DeleteExtension deletes the Extension at index from the Source. It returns an // DeleteExtension deletes the Extension at index from the Source. It return an
// error. // error.
func (s *Source) DeleteExtension(index int) error { func (s *Source) DeleteExtension(index int) error {
if err := deleteFromSlice(&s.Extensions, index); err != nil { if err := deleteFromSlice(&s.Extensions, index); err != nil {

View File

@ -7,10 +7,7 @@ type Text interface {
Check() error Check() error
} }
// NewText creates a new Text. It takes in the strings textType and content and // NewText creates a new Text. It returns a Text.
// returns a Text.
//
// If textType is invalid it returns nil.
func NewText(textType, content string) Text { func NewText(textType, content string) Text {
switch textType { switch textType {
case "text", "": case "text", "":

View File

@ -11,8 +11,7 @@ type XHTMLDiv struct {
Content string `xml:",innerxml"` Content string `xml:",innerxml"`
} }
// NewXHTMLDiv creates a new XHTMLDiv. It takes in a string content and returns // NewXHTMLDiv creates a new XHTMLDiv. It returns a *XHTMLDiv.
// a *XHTMLDiv.
func NewXHTMLDiv(content string) *XHTMLDiv { func NewXHTMLDiv(content string) *XHTMLDiv {
return &XHTMLDiv{ return &XHTMLDiv{
XMLNS: "http://www.w3.org/1999/xhtml", XMLNS: "http://www.w3.org/1999/xhtml",

View File

@ -13,11 +13,10 @@ 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 takes in the strings textType and // newPlainText creates a new PlainText. It returns a *PlainText.
// content and returns a *PlainText.
func newXHTMLText(textType, content string) *XHTMLText { func newXHTMLText(textType, content string) *XHTMLText {
return &XHTMLText{ return &XHTMLText{
CommonAttributes: NewCommonAttributes(), CommonAttributes: newCommonAttributes(),
Type: textType, Type: textType,
XHTMLDiv: NewXHTMLDiv(content), XHTMLDiv: NewXHTMLDiv(content),
} }