29 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
f4dfd6d060 Added more error handling and necessary functions 2024-10-19 12:28:09 +02:00
d4e7bce5e2 Unify error messages 2024-10-18 19:04:08 +02:00
411cd89b7c Added language description to README 2024-10-18 18:00:38 +02:00
574e2331a9 Added a warning to README 2024-10-18 06:13:53 +02:00
24 changed files with 804 additions and 487 deletions

291
README.md
View File

@ -1,136 +1,241 @@
# atom
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 easy creation and extension of elements.
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
easy creation, extension and deletion of elements.
## Installation
To install the latest version of the module, use the following command:
```
```sh
go get git.streifling.com/jason/atom@latest
```
## Usage
This library provides easy to use functions to create and extend elements of an
Atom feed. The intended way of using it entails using these functions.
### Basic Feed
```
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
package main
import (
"fmt"
"log"
"fmt"
"log"
"git.streifling.com/jason/atom"
"git.streifling.com/jason/atom"
)
func main() {
feed, err := atom.NewFeed("Example Feed")
if err != nil {
log.Fatalln(err)
}
feed := atom.NewFeed("Example Feed")
if err := feed.Check(); err != nil {
log.Fatalln(err)
}
author := atom.NewPerson("John Doe")
author.Email = "john.doe@example.com"
feed.AddAuthor(author)
author := atom.NewPerson("John Doe")
author.Email = "john.doe@example.com"
if err := author.Check(); err != nil {
log.Fatalln(err)
}
feed.AddAuthor(author)
entry, err := atom.NewEntry("First Entry")
if err != nil {
log.Fatalln(err)
}
content, err := atom.NewContent(atom.InlineText, "text", "This is the content of the first entry.")
if err != nil {
log.Fatalln(err)
}
entry.Content = content
feed.AddEntry(entry)
entry := atom.NewEntry("First Entry")
entry.Content = atom.NewContent(atom.InlineText, "text", "This is the content of the first entry.")
if err := entry.Check(); err != nil {
log.Fatalln(err)
}
feed.AddEntry(entry)
if err := feed.Check(); err != nil {
log.Fatalln(err)
}
feedString, err := feed.ToXML("utf-8")
if err != nil {
log.Fatalln(err)
}
fmt.Println(feedString)
feedString, err := feed.ToXML("UTF-8")
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 would provide.
It is also possible to use this library in a way similar to what other libraries
provide.
```
```go
package main
import (
"fmt"
"log"
"time"
"fmt"
"log"
"time"
"git.streifling.com/jason/atom"
"git.streifling.com/jason/atom"
"github.com/google/uuid"
)
func main() {
now := time.Now()
now := time.Now()
feed := &atom.Feed{
Title: &atom.PlainText{
Type: "text",
Text: "Example Feed",
},
ID: &atom.ID{URI: "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6"},
Updated: &atom.Date{DateTime: atom.DateTime(now)},
Authors: []*atom.Person{
{
Name: "John Doe",
Email: "john.doe@example.com",
},
},
Entries: []*atom.Entry{
{
Title: &atom.PlainText{
Type: "text",
Text: "First Entry",
},
ID: &atom.ID{URI: "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a"},
Updated: &atom.Date{DateTime: atom.DateTime(now)},
Content: &atom.InlineTextContent{
Type: "text",
Text: "This is the content of the first entry.",
},
},
},
}
feed := &atom.Feed{
Title: &atom.PlainText{
Type: "text",
Text: "Example Feed",
},
ID: &atom.ID{URI: fmt.Sprint("urn:uuid:", uuid.New())},
Updated: &atom.Date{DateTime: atom.DateTime(now)},
Authors: []*atom.Person{
{
Name: "John Doe",
Email: "john.doe@example.com",
},
},
Entries: []*atom.Entry{
{
Title: &atom.PlainText{
Type: "text",
Text: "First Entry",
},
ID: &atom.ID{URI: fmt.Sprint("urn:uuid:", uuid.New())},
Updated: &atom.Date{DateTime: atom.DateTime(now)},
Content: &atom.InlineTextContent{
Type: "text",
Text: "This is the content of the first entry.",
},
},
},
}
feedString, err := feed.ToXML("utf-8")
if err != nil {
log.Fatalln(err)
}
fmt.Println(feedString)
feedString, err := feed.ToXML("UTF-8")
if err != nil {
log.Fatalln(err)
}
fmt.Println(feedString)
}
```
The output of both ways of using it being an RFC4287 compliant Atom feed:
The output of both ways of using it is an RFC4287 compliant Atom feed.
```
<?xml version="1.0" encoding="utf-8"?>
```xml
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<author>
<name>John Doe</name>
<email>john.doe@example.com</email>
</author>
<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
<title type="text">Example Feed</title>
<updated>2024-10-18T05:49:08+02:00</updated>
<entry>
<content type="text">This is the content of the first entry.</content>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
<title type="text">First Entry</title>
<updated>2006-01-02T15:04:05+07:00</updated>
</entry>
<author>
<name>John Doe</name>
<email>john.doe@example.com</email>
</author>
<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
<title type="text">Example Feed</title>
<updated>2024-10-18T05:49:08+02:00</updated>
<entry>
<content type="text">This is the content of the first entry.</content>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
<title type="text">First Entry</title>
<updated>2006-01-02T15:04:05+07:00</updated>
</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>
```

90
atom.go
View File

@ -1,7 +1,9 @@
package atom
import (
"encoding/xml"
"fmt"
"html"
"mime"
"regexp"
"strings"
@ -10,21 +12,41 @@ import (
"golang.org/x/text/language"
)
type (
EmailAddress string
LanguageTag string
MediaType string
IRI string
)
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.
// https://www.w3.org/2011/04/XMLSchema/TypeLibrary-IRI-RFC3987.xsd
func isValidIRI(iri IRI) bool {
func isValidIRI(iri string) bool {
pattern := `((([A-Za-z])[A-Za-z0-9+\-\.]*):((//(((([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽!$&'()*+,;=:]|(%[0-9A-Fa-f][0-9A-Fa-f]))*@))?((\[((((([0-9A-Fa-f]{0,4}:)){6}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|(::(([0-9A-Fa-f]{0,4}:)){5}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|(([0-9A-Fa-f]{0,4})?::(([0-9A-Fa-f]{0,4}:)){4}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:))?[0-9A-Fa-f]{0,4}))?::(([0-9A-Fa-f]{0,4}:)){3}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:)){0,2}[0-9A-Fa-f]{0,4}))?::(([0-9A-Fa-f]{0,4}:)){2}(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:)){0,3}[0-9A-Fa-f]{0,4}))?::[0-9A-Fa-f]{0,4}:(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:)){0,4}[0-9A-Fa-f]{0,4}))?::(([0-9A-Fa-f]{0,4}:[0-9A-Fa-f]{0,4})|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))))|((((([0-9A-Fa-f]{0,4}:)){0,5}[0-9A-Fa-f]{0,4}))?::[0-9A-Fa-f]{0,4})|((((([0-9A-Fa-f]{0,4}:)){0,6}[0-9A-Fa-f]{0,4}))?::))|(v[0-9A-Fa-f]+\.[A-Za-z0-9\-\._~!$&'()*+,;=:]+))\])|(([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5]))\.([0-9]|([1-9][0-9])|(1([0-9]){2})|(2[0-4][0-9])|(25[0-5])))|(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=]))*)((:[0-9]*))?)((/(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))*))*)|(/(((([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))+((/(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))*))*))?)|((([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))+((/(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@]))*))*)|)((\?(([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@])|[-󰀀-󿿽􀀀-􏿽/?])*))?((#((([A-Za-z0-9\-\._~ -퟿豈-﷏ﷰ-￯𐀀-🿽𠀀-𯿽𰀀-𿿽񀀀-񏿽񐀀-񟿽񠀀-񯿽񰀀-񿿽򀀀-򏿽򐀀-򟿽򠀀-򯿽򰀀-򿿽󀀀-󏿽󐀀-󟿽󡀀-󯿽]|(%[0-9A-Fa-f][0-9A-Fa-f])|[!$&'()*+,;=:@])|/|\?))*))?)`
return regexp.MustCompile(pattern).MatchString(string(iri))
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.
func isCorrectlyEscaped(text string) bool {
relevantEntities := []string{"&amp;", "&lt;", "&gt;", "&quot;", "&apos;"}
@ -38,10 +60,10 @@ func isCorrectlyEscaped(text string) bool {
return true
}
// isCompositeMediaType checks whether a string is a composite media type. It
// returns a bool.
func isCompositeMediaType(mediaType string) bool {
mediaType, _, err := mime.ParseMediaType(mediaType)
// isCompositeMediaType checks whether the string m is a composite media type.
// It returns a bool.
func isCompositeMediaType(m string) bool {
mediaType, _, err := mime.ParseMediaType(m)
if err != nil {
return false
}
@ -49,10 +71,10 @@ func isCompositeMediaType(mediaType string) bool {
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.
func isXMLMediaType(mediaType string) bool {
mediaType, _, err := mime.ParseMediaType(mediaType)
func isXMLMediaType(m string) bool {
mediaType, _, err := mime.ParseMediaType(m)
if err != nil {
return false
}
@ -60,35 +82,37 @@ func isXMLMediaType(mediaType string) bool {
return strings.HasSuffix(mediaType, "/xml") || strings.HasSuffix(mediaType, "+xml")
}
// isValidMediaType checks whether a string is a valid media type. It returns a
// bool.
func isValidMediaType(mediaType string) bool {
mediaType, _, err := mime.ParseMediaType(mediaType)
// isValidMediaType checks whether the string m is a valid media type. It
// returns a bool.
func isValidMediaType(m string) bool {
mediaType, _, err := mime.ParseMediaType(m)
if err != nil {
return false
}
typeParts := strings.Split(mediaType, "/")
if len(typeParts) != 2 || typeParts[0] == "" || typeParts[1] == "" {
return false
}
return true
return len(typeParts) == 2 && typeParts[0] != "" && typeParts[1] != ""
}
// isValidLanguageTag checks whether a LanguageTag is valid. It returns a bool.
func isValidLanguageTag(tag LanguageTag) bool {
_, err := language.Parse(string(tag))
// isValidLanguageTag checks whether the string languageTag is valid. It returns
// a bool.
func isValidLanguageTag(languageTag string) bool {
_, err := language.Parse(languageTag)
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 {
regex := regexp.MustCompile(`^[a-zA-Z0-9_]+="[^"]*"$`)
return regex.MatchString(attribute)
return regexp.MustCompile(`^[a-zA-Z0-9_]+="[^"]*"$`).MatchString(attribute)
}
// NewURN generates an new valid IRI based on a UUID. It returns an IRI.
func NewURN() IRI {
return IRI(fmt.Sprint("urn:uuid:", uuid.New()))
func NewURN() string {
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,44 +2,41 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
"html"
)
type Category struct {
XMLName xml.Name `xml:"category"`
*CommonAttributes
Term string `xml:"term,attr"`
Scheme IRI `xml:"scheme,attr,omitempty"`
Label string `xml:"label,attr,omitempty"`
Scheme string `xml:"scheme,attr,omitempty"` // IRI
Label string `xml:"label,attr,omitempty"` // Must be unescaped
}
// NewCategory creates a new Category. It returns a *Category.
// NewCategory creates a new Category. It takes in a string term and returns a
// *Category.
func NewCategory(term string) *Category {
return &Category{Term: term}
}
// SetLabel sets the label of the Category.
func (c *Category) SetLabel(label string) {
c.Label = html.UnescapeString(label)
return &Category{
CommonAttributes: NewCommonAttributes(),
Term: term,
}
}
// Check checks the Category for incompatibilities with RFC4287. It returns an
// error.
func (c *Category) Check() error {
if c.Term == "" {
return errors.New("term attribute of category empty")
return fmt.Errorf("term attribute of category %v empty", c)
}
if c.Scheme != "" {
if !isValidIRI(c.Scheme) {
return fmt.Errorf("scheme attribute %v of category not correctly formatted", c.Scheme)
return fmt.Errorf("scheme attribute of category %v not correctly formatted", c)
}
}
if !isCorrectlyEscaped(c.Label) {
return fmt.Errorf("label attribute %v of category not correctly escaped", c.Label)
return fmt.Errorf("label attribute of category %v not correctly escaped", c)
}
return nil

View File

@ -2,11 +2,12 @@ package atom
import (
"encoding/xml"
"fmt"
)
type CommonAttributes struct {
Base IRI `xml:"base,attr,omitempty"`
Lang LanguageTag `xml:"lang,attr,omitempty"`
Base string `xml:"base,attr,omitempty"` // IRI
Lang string `xml:"lang,attr,omitempty"` // LanguageTag
UndefinedAttributes []*xml.Attr `xml:",attr,omitempty"`
}
@ -16,12 +17,32 @@ func NewCommonAttributes() *CommonAttributes {
return new(CommonAttributes)
}
// AddExtensionAttribute adds the ExtensionAttribute to the CommonAttributes.
func (c *CommonAttributes) AddExtensionAttribute(name, value string) {
if c.UndefinedAttributes == nil {
c.UndefinedAttributes = make([]*xml.Attr, 1)
c.UndefinedAttributes[0] = &xml.Attr{Name: xml.Name{Local: name}, Value: value}
} else {
c.UndefinedAttributes = append(c.UndefinedAttributes, &xml.Attr{Name: xml.Name{Local: name}, Value: value})
}
// AddAttribute adds an attribute to the CommonAttributes. It takes in the
// strings name and value and returns the index as an int.
func (c *CommonAttributes) AddAttribute(name, value string) int {
return addToSlice(&c.UndefinedAttributes, &xml.Attr{Name: xml.Name{Local: name}, Value: value})
}
// DeleteAttribute deletes the attribute at index from the CommonAttributes. It
// returns an error.
func (c *CommonAttributes) DeleteAttribute(index int) error {
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 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
}

View File

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

11
date.go
View File

@ -10,15 +10,18 @@ type Date struct {
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.
func DateTime(t time.Time) string {
return string(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 {
return &Date{DateTime: DateTime(t)}
return &Date{
CommonAttributes: NewCommonAttributes(),
DateTime: DateTime(t),
}
}
// Check checks the Date for incompatibilities with RFC4287. It returns an

170
entry.go
View File

@ -2,7 +2,6 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
"strings"
"time"
@ -31,25 +30,26 @@ type Entry struct {
}
// 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
// the atom:entry contains an atom:source element that contains an atom:author
// element or, in an Atom Feed Document, the atom:feed element contains an
// atom:author element itself.
func (e *Entry) checkAuthors(authorInFeed bool) error {
func (e *Entry) checkAuthors(authorIsInFeed bool) error {
if e.Authors == nil {
if !authorInFeed {
if !authorIsInFeed {
if e.Source == nil {
return errors.New("no authors set in entry")
return fmt.Errorf("no authors set in entry %v", e.ID.URI)
}
if e.Source.Authors == nil {
return errors.New("no authors set in entry")
return fmt.Errorf("no authors set in entry %v", e.ID.URI)
}
}
} else {
for i, a := range e.Authors {
if err := a.Check(); err != nil {
return fmt.Errorf("author element %v of entry: %v", i, err)
return fmt.Errorf("author element %v of entry %v: %v", i, e.ID.URI, err)
}
}
}
@ -57,93 +57,120 @@ func (e *Entry) checkAuthors(authorInFeed bool) error {
return nil
}
// NewEntry creates a new Entry. It returns a *Entry and an error.
func NewEntry(title string) (*Entry, error) {
text, err := NewText("text", title)
if err != nil {
return nil, fmt.Errorf("error creating new entry: %v", err)
}
id, err := NewID(NewURN())
if err != nil {
return nil, fmt.Errorf("error creating new entry: %v", err)
// update sets the Updated time to time.Now.
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 {
return &Entry{
ID: id,
Title: text,
Updated: NewDate(time.Now()),
}, nil
CommonAttributes: NewCommonAttributes(),
ID: NewID(NewURN()),
Title: NewText("text", title),
Updated: NewDate(time.Now()),
}
}
// AddAuthor adds the Person as an author to the Entry.
func (e *Entry) AddAuthor(p *Person) {
if e.Authors == nil {
e.Authors = make([]*Person, 1)
e.Authors[0] = p
} else {
e.Authors = append(e.Authors, p)
}
e.Updated.DateTime = DateTime(time.Now())
// AddAuthor adds the Person a as an author to the Entry. It returns the index
// as an int.
func (e *Entry) AddAuthor(a *Person) int {
e.update()
return addToSlice(&e.Authors, a)
}
// AddCategory adds the Category to the Entry.
func (e *Entry) AddCategory(c *Category) {
if e.Categories == nil {
e.Categories = make([]*Category, 1)
e.Categories[0] = c
} else {
e.Categories = append(e.Categories, c)
// 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)
}
e.Updated.DateTime = DateTime(time.Now())
e.update()
return nil
}
// AddContributor adds the Person as a contributor to the Entry.
func (e *Entry) AddContributor(c *Person) {
if e.Contributors == nil {
e.Contributors = make([]*Person, 1)
e.Contributors[0] = c
} else {
e.Contributors = append(e.Contributors, c)
}
e.Updated.DateTime = DateTime(time.Now())
// AddCategory adds the Category c to the Entry. It returns the index as an int.
func (e *Entry) AddCategory(c *Category) int {
e.update()
return addToSlice(&e.Categories, c)
}
// AddLink adds the Link to the Entry.
func (e *Entry) AddLink(l *Link) {
if e.Links == nil {
e.Links = make([]*Link, 1)
e.Links[0] = l
} else {
e.Links = append(e.Links, l)
// DeleteCategory deletes the Category at index from the Entry. It returns an
// error.
func (e *Entry) DeleteCategory(index int) error {
if err := deleteFromSlice(&e.Categories, index); err != nil {
return fmt.Errorf("error deleting category %v from entry %v: %v", index, e.ID.URI, err)
}
e.Updated.DateTime = DateTime(time.Now())
e.update()
return nil
}
// AddExtension adds the ExtensionElement to the Entry.
func (e *Entry) AddExtension(x *ExtensionElement) {
if e.Extensions == nil {
e.Extensions = make([]*ExtensionElement, 1)
e.Extensions[0] = x
} else {
e.Extensions = append(e.Extensions, x)
// AddContributor adds the Person c as a contributor to the Entry. It returns
// the index as an int.
func (e *Entry) AddContributor(c *Person) int {
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)
}
e.Updated.DateTime = DateTime(time.Now())
e.update()
return nil
}
// AddLink adds the Link l to the Entry. It returns the index as an int.
func (e *Entry) AddLink(l *Link) int {
e.update()
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)
}
e.update()
return nil
}
// 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.update()
return nil
}
// Check checks the Entry for incompatibilities with RFC4287. It returns an
// error.
func (e *Entry) Check() error {
if e.ID == nil {
return errors.New("no id element of entry")
return fmt.Errorf("no id element of entry %v", e)
} else {
if err := e.ID.Check(); err != nil {
return fmt.Errorf("id element of entry: %v", err)
return fmt.Errorf("id element of entry %v: %v", e, err)
}
}
@ -166,7 +193,7 @@ func (e *Entry) Check() error {
// contain at least one atom:link element with a rel attribute value of
// "alternate".
if !alternateRelExists(e.Links) {
return errors.New("no content element of entry %v and no link element with rel \"alternate\"")
return fmt.Errorf("no content element of entry %v and no link element with rel \"alternate\"", e.ID.URI)
}
}
@ -182,7 +209,7 @@ func (e *Entry) Check() error {
}
}
if hasAlternateDuplicateLinks(e.Links) {
return fmt.Errorf("links with with a rel attribute value of \"alternate\" and duplicate type and hreflang attribute values found in entry %v", e.ID.URI)
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 {
@ -250,11 +277,12 @@ func (e *Entry) Check() error {
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) {
xml, err := xml.MarshalIndent(e, "", " ")
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

View File

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

203
feed.go
View File

@ -2,7 +2,6 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
"time"
)
@ -26,105 +25,161 @@ type Feed struct {
Entries []*Entry `xml:",omitempty"`
}
// NewFeed creates a new Feed. It returns a *Feed and an error.
func NewFeed(title string) (*Feed, error) {
text, err := NewText("text", title)
if err != nil {
return nil, fmt.Errorf("error creating new feed: %v", err)
}
id, err := NewID(NewURN())
if err != nil {
return nil, fmt.Errorf("error creating new feed: %v", err)
// update sets the Updated time to time.Now.
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 {
return &Feed{
ID: id,
Title: text,
Updated: NewDate(time.Now()),
}, nil
CommonAttributes: NewCommonAttributes(),
ID: NewID(NewURN()),
Title: NewText("text", title),
Updated: NewDate(time.Now()),
}
}
// AddAuthor adds the Person as an author to the Feed.
func (f *Feed) AddAuthor(p *Person) {
if f.Authors == nil {
f.Authors = make([]*Person, 1)
f.Authors[0] = p
} else {
f.Authors = append(f.Authors, p)
}
f.Updated.DateTime = DateTime(time.Now())
// AddAuthor adds the Person a as an author to the Feed. It returns the index as
// an int.
func (f *Feed) AddAuthor(a *Person) int {
f.update()
return addToSlice(&f.Authors, a)
}
// AddCategory adds the Category to the Feed.
func (f *Feed) AddCategory(c *Category) {
if f.Categories == nil {
f.Categories = make([]*Category, 1)
f.Categories[0] = c
} else {
f.Categories = append(f.Categories, c)
// 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)
}
f.Updated.DateTime = DateTime(time.Now())
f.update()
return nil
}
// AddContributor adds the Person as a contributor to the Feed.
func (f *Feed) AddContributor(c *Person) {
if f.Contributors == nil {
f.Contributors = make([]*Person, 1)
f.Contributors[0] = c
} else {
f.Contributors = append(f.Contributors, c)
}
f.Updated.DateTime = DateTime(time.Now())
// AddCategory adds the Category c to the Feed. It returns the index as an int.
func (f *Feed) AddCategory(c *Category) int {
f.update()
return addToSlice(&f.Categories, c)
}
// AddLink adds the Link to the Feed. There should be one Link with Rel "self".
func (f *Feed) AddLink(l *Link) {
if f.Links == nil {
f.Links = make([]*Link, 1)
f.Links[0] = l
} else {
f.Links = append(f.Links, l)
// DeleteCategory deletes the Category at index from the Feed. It returns an
// error.
func (f *Feed) DeleteCategory(index int) error {
if err := deleteFromSlice(&f.Categories, index); err != nil {
return fmt.Errorf("error deleting category %v from entry %v: %v", index, f.ID.URI, err)
}
f.Updated.DateTime = DateTime(time.Now())
f.update()
return nil
}
// AddExtension adds the Extension to the Feed.
func (f *Feed) AddExtension(e *ExtensionElement) {
if f.Extensions == nil {
f.Extensions = make([]*ExtensionElement, 1)
f.Extensions[0] = e
} else {
f.Extensions = append(f.Extensions, e)
}
f.Updated.DateTime = DateTime(time.Now())
// AddContributor adds the Person c as a contributor to the Feed. It returns the
// index as an int.
func (f *Feed) AddContributor(c *Person) int {
f.update()
return addToSlice(&f.Contributors, c)
}
// AddEntry adds the Entry to the Feed.
func (f *Feed) AddEntry(e *Entry) {
if f.Entries == nil {
f.Entries = make([]*Entry, 1)
f.Entries[0] = e
} else {
f.Entries = append(f.Entries, e)
// 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)
}
f.Updated.DateTime = DateTime(time.Now())
f.update()
return nil
}
// AddLink adds the Link l to the Feed. It returns the index as an int.
//
// There should be one Link with Rel "self".
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)
}
f.update()
return nil
}
// AddExtension adds the Extension e to the Feed. It returns the index as an
// int.
func (f *Feed) AddExtension(e *ExtensionElement) int {
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)
}
f.update()
return nil
}
// 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.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
}
// Check checks the Feed for incompatibilities with RFC4287. It returns an
// error.
func (f *Feed) Check() error {
if f.ID == nil {
return errors.New("no id element of feed")
return fmt.Errorf("no id element of feed %v", f)
} else {
if err := f.ID.Check(); err != nil {
return fmt.Errorf("id element of feed: %v", err)
return fmt.Errorf("id element of feed %v: %v", f, err)
}
}
@ -175,7 +230,7 @@ func (f *Feed) Check() error {
}
}
if hasAlternateDuplicateLinks(f.Links) {
return fmt.Errorf("links with with a rel attribute value of \"alternate\" and duplicate type and hreflang attribute values found in feed %v", f.ID.URI)
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 {
@ -205,7 +260,7 @@ func (f *Feed) Check() error {
}
if f.Updated == nil {
return fmt.Errorf("no updated element of feed %v", f.ID)
return fmt.Errorf("no updated element of feed %v", f.ID.URI)
} else {
if err := f.Updated.Check(); err != nil {
return fmt.Errorf("updated element of feed %v: %v", f.ID.URI, err)
@ -231,7 +286,7 @@ func (f *Feed) Check() error {
func (f *Feed) ToXML(encoding string) (string, error) {
xml, err := xml.MarshalIndent(f, "", " ")
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

View File

@ -2,7 +2,6 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
"html"
)
@ -10,14 +9,18 @@ import (
type Generator struct {
XMLName xml.Name `xml:"generator"`
*CommonAttributes
URI IRI `xml:"uri,attr,omitempty"`
URI string `xml:"uri,attr,omitempty"` // IRI
Version string `xml:"version,attr,omitempty"`
Text string `xml:",chardata"`
}
// NewGenerator creates a new Generator. It returns a *Generator.
// NewGenerator creates a new Generator. It takes in a string text and returns a
// *Generator.
func NewGenerator(text string) *Generator {
return &Generator{Text: html.UnescapeString(text)}
return &Generator{
CommonAttributes: NewCommonAttributes(),
Text: html.UnescapeString(text),
}
}
// Check checks the Generator for incompatibilities with RFC4287. It returns an
@ -25,16 +28,16 @@ func NewGenerator(text string) *Generator {
func (g *Generator) Check() error {
if g.URI != "" {
if !isValidIRI(g.URI) {
return fmt.Errorf("uri attribute %v of generator not correctly formatted", g.URI)
return fmt.Errorf("uri attribute of generator %v not correctly formatted", g)
}
}
if g.Text == "" {
return errors.New("text element of generator empty")
return fmt.Errorf("text element of generator %v empty", g)
}
if !isCorrectlyEscaped(g.Text) {
return fmt.Errorf("text element %v of generator not correctly escaped", g.Text)
return fmt.Errorf("text element of generator %v not correctly escaped", g)
}
return nil

15
icon.go
View File

@ -9,16 +9,15 @@ import (
type Icon struct {
XMLName xml.Name `xml:"icon"`
*CommonAttributes
URI IRI `xml:",chardata"`
URI string `xml:",chardata"` // IRI
}
// NewIcon creates a new Icon. It returns a *Icon and an error.
func NewIcon(uri IRI) (*Icon, error) {
if !isValidIRI(uri) {
return nil, fmt.Errorf("uri %v not correctly formatted", uri)
// NewIcon creates a new Icon. It takes in a string uri and returns a *Icon.
func NewIcon(uri string) *Icon {
return &Icon{
CommonAttributes: NewCommonAttributes(),
URI: uri,
}
return &Icon{URI: uri}, nil
}
// Check checks the Icon for incompatibilities with RFC4287. It returns an
@ -28,7 +27,7 @@ func (i *Icon) Check() error {
return errors.New("uri element of icon empty")
} else {
if !isValidIRI(i.URI) {
return fmt.Errorf("uri attribute %v of icon not correctly formatted", i.URI)
return fmt.Errorf("uri attribute of icon %v not correctly formatted", i)
}
}

15
id.go
View File

@ -9,16 +9,15 @@ import (
type ID struct {
XMLName xml.Name `xml:"id"`
*CommonAttributes
URI IRI `xml:",chardata"`
URI string `xml:",chardata"` // IRI
}
// NewID creates a new ID. It returns a *ID and an error.
func NewID(uri IRI) (*ID, error) {
if !isValidIRI(uri) {
return nil, fmt.Errorf("uri %v not correctly formatted", uri)
// NewID creates a new ID. It takes in a string uri and returns a *ID.
func NewID(uri string) *ID {
return &ID{
CommonAttributes: NewCommonAttributes(),
URI: uri,
}
return &ID{URI: IRI(uri)}, nil
}
// Check checks the ID for incompatibilities with RFC4287. It returns an error.
@ -27,7 +26,7 @@ func (i *ID) Check() error {
return errors.New("uri element of id empty")
} else {
if !isValidIRI(i.URI) {
return fmt.Errorf("uri element %v of id not correctly formatted", i.URI)
return fmt.Errorf("uri element of id %v not correctly formatted", i)
}
}

View File

@ -2,7 +2,6 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
"mime"
)
@ -10,18 +9,20 @@ import (
type InlineOtherContent struct {
XMLName xml.Name `xml:"content"`
*CommonAttributes
AnyElement any `xml:",chardata"`
Type MediaType `xml:"type,attr,omitempty"`
AnyElement any `xml:",chardata"`
Type string `xml:"type,attr,omitempty"` // MediaType
}
// newInlineOtherContent creates a new InlineOtherContent. It returns a
// *InlineOtherContent and an error.
func newInlineOtherContent(mediaType string, content any) (*InlineOtherContent, error) {
if mediaType, _, err := mime.ParseMediaType(mediaType); err != nil {
return nil, fmt.Errorf("media type %v incompatible with inline other content", mediaType)
}
// newInlineOtherContent creates a new InlineOtherContent. It takes in the string
// mediaType and any content and returns a *InlineOtherContent and an error.
func newInlineOtherContent(mediaType string, content any) *InlineOtherContent {
mediaType, _, _ = mime.ParseMediaType(mediaType)
return &InlineOtherContent{Type: MediaType(mediaType), AnyElement: content}, nil
return &InlineOtherContent{
CommonAttributes: NewCommonAttributes(),
Type: mediaType,
AnyElement: content,
}
}
// isContent checks whether the InlineOtherContent is a Content. It returns a
@ -33,19 +34,19 @@ func (i *InlineOtherContent) isContent() bool { return true }
func (i *InlineOtherContent) hasSRC() bool { return false }
// getType returns the Type of the InlineOtherContent as a string.
func (i *InlineOtherContent) getType() string { return string(i.Type) }
func (i *InlineOtherContent) getType() string { return i.Type }
// Check checks the InlineOtherContent for incompatibilities with RFC4287. It
// returns an error.
func (i *InlineOtherContent) Check() error {
mediaType := i.getType()
if mediaType, _, err := mime.ParseMediaType(mediaType); err != nil {
return fmt.Errorf("type attribute %v incompatible with inline other content", mediaType)
if !isValidMediaType(mediaType) {
return fmt.Errorf("type attribute of inline other content %v invalid media type", i)
}
if isCompositeMediaType(mediaType) {
return errors.New("type attribute of inline other content must not be a composite type")
return fmt.Errorf("type attribute of inline other content %v must not be a composite type", i)
}
return nil

View File

@ -2,7 +2,6 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
)
@ -13,19 +12,14 @@ type InlineTextContent struct {
Text string `xml:",chardata"`
}
// newInlineTextContent creates a new InlineTextContent. It returns a
// *InlineTextContent and an error.
func newInlineTextContent(mediaType string, content any) (*InlineTextContent, error) {
if mediaType != "text" && mediaType != "html" && mediaType != "" {
return nil, fmt.Errorf("media type %v incompatible with inline text content", mediaType)
// newInlineTextContent creates a new InlineTextContent. It takes in the strings
// mediaType and text and returns a *InlineTextContent.
func newInlineTextContent(mediaType, text string) *InlineTextContent {
return &InlineTextContent{
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
@ -43,7 +37,7 @@ func (i *InlineTextContent) getType() string { return i.Type }
// returns an error.
func (i *InlineTextContent) Check() error {
if i.Type != "" && i.Type != "text" && i.Type != "html" {
return errors.New("type attribute of inline text content must be text or html if not omitted")
return fmt.Errorf("type attribute of inline text content %v must be text or html if not omitted", i)
}
return nil

View File

@ -2,7 +2,6 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
)
@ -13,19 +12,14 @@ type InlineXHTMLContent struct {
Type string `xml:"type,attr"`
}
// newInlineXHTMLContent creates a new InlineXHTMLContent. It returns a
// *InlineXHTMLContent and an error.
func newInlineXHTMLContent(mediaType string, content any) (*InlineXHTMLContent, error) {
if mediaType != "xhtml" {
return nil, fmt.Errorf("media type %v incompatible with inline xhtml content", mediaType)
// newInlineXHTMLContent creates a new InlineXHTMLContent. It takes in the
// string mediaType and the XHTMLDiv div and returns a *InlineXHTMLContent.
func newInlineXHTMLContent(mediaType string, div *XHTMLDiv) *InlineXHTMLContent {
return &InlineXHTMLContent{
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
@ -43,11 +37,11 @@ func (i *InlineXHTMLContent) getType() string { return i.Type }
// returns an error.
func (i *InlineXHTMLContent) Check() error {
if i.Type != "xhtml" {
return errors.New("type attribute of inline xhtml content must be xhtml")
return fmt.Errorf("type attribute of inline xhtml content %v must be xhtml", i)
}
if err := i.XHTMLDiv.Check(); err != nil {
return fmt.Errorf("xhtml div element %v of inline xhtml content %v: %v", i.XHTMLDiv, i, err)
return fmt.Errorf("xhtml div element of inline xhtml content %v: %v", i, err)
}
return nil

34
link.go
View File

@ -2,7 +2,6 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
"strings"
)
@ -10,45 +9,48 @@ import (
type Link struct {
XMLName xml.Name `xml:"link"`
*CommonAttributes
Title string `xml:"title,attr,omitempty"`
Href IRI `xml:"href,attr"`
Rel string `xml:"rel,attr,omitempty"`
Type MediaType `xml:"type,attr,omitempty"`
HrefLang LanguageTag `xml:"hreflang,attr,omitempty"`
Length uint `xml:"length,attr,omitempty"`
Title string `xml:"title,attr,omitempty"`
Href string `xml:"href,attr"` // IRI
Rel string `xml:"rel,attr,omitempty"`
Type string `xml:"type,attr,omitempty"` // MediaType
HrefLang string `xml:"hreflang,attr,omitempty"` // LanguageTag
Length uint `xml:"length,attr,omitempty"`
}
// NewLink creates a new Link. It returns a *Link.
// NewLink creates a new Link. It takes in the string href and returns a *Link.
func NewLink(href string) *Link {
return &Link{Href: IRI(href)}
return &Link{
CommonAttributes: NewCommonAttributes(),
Href: href,
}
}
// Check checks the Link for incompatibilities with RFC4287. It returns an
// error.
func (l *Link) Check() error {
if l.Href == "" {
return errors.New("href attribute of link empty")
return fmt.Errorf("href attribute of link %v empty", l)
} else {
if !isValidIRI(l.Href) {
return fmt.Errorf("href attribute %v of link not correctly formatted", l.Href)
return fmt.Errorf("href attribute of link %v not correctly formatted", l)
}
}
if l.Rel != "" {
if strings.Contains(l.Rel, ":") && !isValidIRI(IRI(l.Rel)) {
return fmt.Errorf("rel attribute %v of link %v not correctly formatted", l.Rel, l.Href)
if strings.Contains(l.Rel, ":") && !isValidIRI(l.Rel) {
return fmt.Errorf("rel attribute of link %v not correctly formatted", l)
}
}
if l.Type != "" {
if !isValidMediaType(string(l.Type)) {
return fmt.Errorf("type attribute %v of link %v invalid media type", l.Type, l.Href)
if !isValidMediaType(l.Type) {
return fmt.Errorf("type attribute of link %v invalid media type", l)
}
}
if l.HrefLang != "" {
if !isValidLanguageTag(l.HrefLang) {
return fmt.Errorf("hreflang attribute %v of link %v invalid language tag", l.Type, l.HrefLang)
return fmt.Errorf("hreflang attribute of link %v invalid language tag", l)
}
}

18
logo.go
View File

@ -2,33 +2,31 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
)
type Logo struct {
XMLName xml.Name `xml:"logo"`
*CommonAttributes
URI IRI `xml:",chardata"`
URI string `xml:",chardata"` // IRI
}
// NewLogo creates a new Logo. It returns a *Logo.
func NewLogo(uri IRI) (*Logo, error) {
if !isValidIRI(uri) {
return nil, fmt.Errorf("uri %v not correctly formatted", uri)
// NewLogo creates a new Logo. It takes in a string uri and returns a *Logo.
func NewLogo(uri string) *Logo {
return &Logo{
CommonAttributes: NewCommonAttributes(),
URI: uri,
}
return &Logo{URI: uri}, nil
}
// Check checks the Logo for incompatibilities with RFC4287. It returns an
// error.
func (l *Logo) Check() error {
if l.URI == "" {
return errors.New("uri element of logo empty")
return fmt.Errorf("uri element of logo %v empty", l)
} else {
if !isValidIRI(l.URI) {
return fmt.Errorf("uri element %v of logo not correctly formatted", l.URI)
return fmt.Errorf("uri element of logo %v not correctly formatted", l)
}
}

View File

@ -2,7 +2,6 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
"mime"
)
@ -10,27 +9,20 @@ import (
type OutOfLineContent struct {
XMLName xml.Name `xml:"content"`
*CommonAttributes
Type MediaType `xml:"type,attr,omitempty"`
SRC IRI `xml:"src,attr"`
Type string `xml:"type,attr,omitempty"` // MediaType
SRC string `xml:"src,attr"` // IRI
}
// newOutOfLineContent creates a new OutOfLineContent. It returns a
// *OutOfLineContent and an error.
func newOutOfLineContent(mediaType string, content any) (*OutOfLineContent, error) {
if mediaType, _, err := mime.ParseMediaType(mediaType); err != nil {
return nil, fmt.Errorf("media type %v incompatible with out of line content", mediaType)
}
// newOutOfLineContent creates a new OutOfLineContent. It takes in the strings
// mediaType and src and returns a *OutOfLineContent.
func newOutOfLineContent(mediaType, src string) *OutOfLineContent {
mediaType, _, _ = mime.ParseMediaType(mediaType)
iri, ok := content.(IRI)
if !ok {
return nil, fmt.Errorf("content type %T incompatible with out of line content", content)
return &OutOfLineContent{
CommonAttributes: NewCommonAttributes(),
Type: mediaType,
SRC: src,
}
if !isValidIRI(iri) {
return nil, errors.New("content not a valid uri")
}
return &OutOfLineContent{Type: MediaType(mediaType), SRC: iri}, nil
}
// isContent checks whether the OutOfLineContent is a Content. It returns a
@ -42,23 +34,23 @@ func (o *OutOfLineContent) isContent() bool { return true }
func (o *OutOfLineContent) hasSRC() bool { return true }
// getType returns the Type of the OutOfLineContent as a string.
func (o *OutOfLineContent) getType() string { return string(o.Type) }
func (o *OutOfLineContent) getType() string { return o.Type }
// Check checks the OutOfLineContent for incompatibilities with RFC4287. It
// returns an error.
func (o *OutOfLineContent) Check() error {
mediaType := o.getType()
if mediaType, _, err := mime.ParseMediaType(mediaType); err != nil {
return fmt.Errorf("type attribute %v incompatible with out of line content", mediaType)
if !isValidMediaType(mediaType) {
return fmt.Errorf("type attribute of out of line content %v invalid media type", o)
}
if isCompositeMediaType(mediaType) {
return errors.New("type attribute of out of line content must not be a composite type")
return fmt.Errorf("type attribute of out of line content %v must not be a composite type", o)
}
if o.SRC == "" {
return errors.New("src attribute of out of line content empty")
return fmt.Errorf("src attribute of out of line content %v empty", o)
}
return nil

View File

@ -1,7 +1,6 @@
package atom
import (
"errors"
"fmt"
"net/mail"
)
@ -9,50 +8,57 @@ import (
type Person struct {
*CommonAttributes
Name string `xml:"name"`
URI IRI `xml:"uri,omitempty"`
Email EmailAddress `xml:"email,omitempty"`
URI string `xml:"uri,omitempty"` // IRI
Email string `xml:"email,omitempty"` // EmailAddress
Extensions []*ExtensionElement `xml:",any,omitempty"`
}
// NewPerson creates a new Person. It returns a *Person.
// NewPerson creates a new Person. It takes in a string name and returns a
// *Person.
func NewPerson(name string) *Person {
return &Person{Name: name}
return &Person{
CommonAttributes: NewCommonAttributes(),
Name: name,
}
}
// AddExtension adds the Extension to the Person.
func (p *Person) AddExtension(e *ExtensionElement) {
if p.Extensions == nil {
p.Extensions = make([]*ExtensionElement, 1)
p.Extensions[0] = e
} else {
p.Extensions = append(p.Extensions, e)
// AddExtension adds the Extension e to the Person. It returns the index as an
// int.
func (p *Person) AddExtension(e *ExtensionElement) int {
return addToSlice(&p.Extensions, e)
}
// DeleteExtension deletes the Extension at index from the Person. It returns an
// error.
func (p *Person) DeleteExtension(index int) error {
if err := deleteFromSlice(&p.Extensions, index); err != nil {
return fmt.Errorf("error deleting extension %v from person %v: %v", index, p, err)
}
return nil
}
// Check checks the Person for incompatibilities with RFC4287. It returns an
// error.
func (p *Person) Check() error {
if p.Name == "" {
return errors.New("name element of person element empty")
return fmt.Errorf("name element of person %v empty", p)
}
if p.URI != "" {
if !isValidIRI(p.URI) {
return fmt.Errorf("uri element of person %v not correctly formatted", p.Name)
return fmt.Errorf("uri element of person %v not correctly formatted", p)
}
}
if p.Email != "" {
if _, err := mail.ParseAddress(string(p.Email)); err != nil {
return fmt.Errorf("email element of person %v not correctly formatted", p.Name)
if _, err := mail.ParseAddress(p.Email); err != nil {
return fmt.Errorf("email element of person %v not correctly formatted", p)
}
}
if p.Extensions != nil {
for i, e := range p.Extensions {
if err := e.Check(); err != nil {
return fmt.Errorf("extension element %v of person %v: %v", i, p.Name, err)
}
for i, e := range p.Extensions {
if err := e.Check(); err != nil {
return fmt.Errorf("extension element %v of person %v: %v", i, p, err)
}
}

View File

@ -1,6 +1,8 @@
package atom
import "errors"
import (
"fmt"
)
type PlainText struct {
*CommonAttributes
@ -11,19 +13,29 @@ type PlainText struct {
// isText checks whether the PlainText is a Text. It returns a bool.
func (p *PlainText) isText() bool { return true }
// newPlainText creates a new PlainText. It takes in the strings textType and
// content and returns a *PlainText.
func newPlainText(textType, content string) *PlainText {
return &PlainText{
CommonAttributes: NewCommonAttributes(),
Type: textType,
Text: content,
}
}
// Check checks the PlainText for incompatibilities with RFC4287. It returns an
// error.
func (p *PlainText) Check() error {
if p.Type != "" && p.Type != "text" && p.Type != "html" {
return errors.New("type attribute of plain text must be text or html if not omitted")
return fmt.Errorf("type attribute of plain text %v must be text or html if not omitted", p)
}
if p.Type == "html" && !isCorrectlyEscaped(p.Text) {
return errors.New("text element of plain text not correctly escaped")
return fmt.Errorf("text element of plain text %v not correctly escaped", p)
}
if p.Text == "" {
return errors.New("text element of plain text empty")
return fmt.Errorf("text element of plain text %v empty", p)
}
return nil

103
source.go
View File

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

26
text.go
View File

@ -1,31 +1,25 @@
package atom
import (
"fmt"
"html"
)
import "html"
type Text interface {
isText() bool
Check() error
}
// NewText creates a new Text. It returns a Text and an error.
func NewText(textType, content string) (Text, error) {
// NewText creates a new Text. It takes in the strings textType and content and
// returns a Text.
//
// If textType is invalid it returns nil.
func NewText(textType, content string) Text {
switch textType {
case "text", "":
return &PlainText{Type: textType, Text: content}, nil
return newPlainText(textType, content)
case "html":
return &PlainText{Type: textType, Text: html.UnescapeString(content)}, nil
return newPlainText(textType, html.UnescapeString(content))
case "xhtml":
return &XHTMLText{
Type: textType,
XHTMLDiv: &XHTMLDiv{
XMLNS: "http://www.w3.org/1999/xhtml",
Content: content,
},
}, nil
return newXHTMLText(textType, content)
default:
return nil, fmt.Errorf("%v is not a valid text type", textType)
return nil
}
}

View File

@ -2,7 +2,7 @@ package atom
import (
"encoding/xml"
"errors"
"fmt"
)
type XHTMLDiv struct {
@ -11,7 +11,8 @@ type XHTMLDiv struct {
Content string `xml:",innerxml"`
}
// NewXHTMLDiv creates a new XHTMLDiv. It returns a *XHTMLDiv.
// NewXHTMLDiv creates a new XHTMLDiv. It takes in a string content and returns
// a *XHTMLDiv.
func NewXHTMLDiv(content string) *XHTMLDiv {
return &XHTMLDiv{
XMLNS: "http://www.w3.org/1999/xhtml",
@ -23,7 +24,7 @@ func NewXHTMLDiv(content string) *XHTMLDiv {
// error.
func (x *XHTMLDiv) Check() error {
if x.XMLNS != "http://www.w3.org/1999/xhtml" {
return errors.New("xmlns attribute of xhtml text must be http://www.w3.org/1999/xhtml")
return fmt.Errorf("xmlns attribute of xhtml text %v must be http://www.w3.org/1999/xhtml", x)
}
return nil

View File

@ -1,7 +1,6 @@
package atom
import (
"errors"
"fmt"
)
@ -14,11 +13,21 @@ type XHTMLText struct {
// isText checks whether the XHTMLText is a Text. It returns a bool.
func (x *XHTMLText) isText() bool { return true }
// newPlainText creates a new PlainText. It takes in the strings textType and
// content and returns a *PlainText.
func newXHTMLText(textType, content string) *XHTMLText {
return &XHTMLText{
CommonAttributes: NewCommonAttributes(),
Type: textType,
XHTMLDiv: NewXHTMLDiv(content),
}
}
// Check checks the XHTMLText for incompatibilities with RFC4287. It returns an
// error.
func (x *XHTMLText) Check() error {
if x.Type != "xhtml" {
return errors.New("type attribute of xhtml text must be xhtml")
return fmt.Errorf("type attribute of xhtml text %v must be xhtml", x)
}
if err := x.XHTMLDiv.Check(); err != nil {