138 lines
3.2 KiB
Go
138 lines
3.2 KiB
Go
package rss
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
// Parse parses an RSS 2.0 or Atom 1.0 XML string into a unified Feed struct.
|
|
// It auto-detects the feed format by inspecting the root XML element.
|
|
func Parse(data string) (*Feed, error) {
|
|
trimmed := strings.TrimSpace(data)
|
|
if trimmed == "" {
|
|
return nil, fmt.Errorf("empty input")
|
|
}
|
|
|
|
format, err := detectFormat([]byte(trimmed))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b := []byte(trimmed)
|
|
switch format {
|
|
case "rss":
|
|
return parseRSS(b)
|
|
case "atom":
|
|
return parseAtom(b)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported feed format: %s", format)
|
|
}
|
|
}
|
|
|
|
// Validate checks whether the input string is a valid RSS 2.0 or Atom 1.0 feed.
|
|
// Returns nil on success, or a descriptive error explaining what is wrong.
|
|
//
|
|
// Process convention:
|
|
// - success → true (bool)
|
|
// - failure → error description string
|
|
func Validate(data string) error {
|
|
trimmed := strings.TrimSpace(data)
|
|
if trimmed == "" {
|
|
return fmt.Errorf("empty input: expected an XML document containing an RSS or Atom feed")
|
|
}
|
|
|
|
// Step 1: check if it is valid XML at all
|
|
decoder := xml.NewDecoder(bytes.NewReader([]byte(trimmed)))
|
|
var rootFound bool
|
|
for {
|
|
tok, err := decoder.Token()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("not valid XML: %s", err.Error())
|
|
}
|
|
if _, ok := tok.(xml.StartElement); ok {
|
|
rootFound = true
|
|
break
|
|
}
|
|
}
|
|
if !rootFound {
|
|
return fmt.Errorf("not valid XML: no root element found")
|
|
}
|
|
|
|
// Step 2: detect format
|
|
format, err := detectFormat([]byte(trimmed))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Step 3: attempt a full parse to verify structural integrity
|
|
b := []byte(trimmed)
|
|
switch format {
|
|
case "rss":
|
|
feed, err := parseRSS(b)
|
|
if err != nil {
|
|
return fmt.Errorf("RSS 2.0 parse error: %s", err.Error())
|
|
}
|
|
if feed.Title == "" {
|
|
return fmt.Errorf("RSS 2.0 feed is missing required <title> element in <channel>")
|
|
}
|
|
case "atom":
|
|
feed, err := parseAtom(b)
|
|
if err != nil {
|
|
return fmt.Errorf("Atom 1.0 parse error: %s", err.Error())
|
|
}
|
|
if feed.Title == "" {
|
|
return fmt.Errorf("Atom feed is missing required <title> element")
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported feed format: %s", format)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// detectFormat inspects the root XML element to determine the feed format.
|
|
// Returns "rss" for RSS 2.0, "atom" for Atom 1.0, or an error.
|
|
func detectFormat(data []byte) (string, error) {
|
|
decoder := xml.NewDecoder(bytes.NewReader(data))
|
|
for {
|
|
tok, err := decoder.Token()
|
|
if err == io.EOF {
|
|
return "", fmt.Errorf("not valid XML: unexpected end of document before root element")
|
|
}
|
|
if err != nil {
|
|
return "", fmt.Errorf("not valid XML: %s", err.Error())
|
|
}
|
|
|
|
se, ok := tok.(xml.StartElement)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
local := strings.ToLower(se.Name.Local)
|
|
|
|
switch {
|
|
case local == "rss":
|
|
return "rss", nil
|
|
|
|
case local == "rdf":
|
|
// RDF-based RSS 1.0 (root element is <rdf:RDF>)
|
|
// We treat it as RSS for parsing purposes
|
|
return "rss", nil
|
|
|
|
case local == "feed":
|
|
return "atom", nil
|
|
|
|
default:
|
|
return "", fmt.Errorf(
|
|
"unrecognized feed format: root element is <%s>, expected <rss>, <feed>, or <rdf:RDF>",
|
|
se.Name.Local,
|
|
)
|
|
}
|
|
}
|
|
}
|