Skip to content

Commit ae40b80

Browse files
committed
extensions + simpler basic structs
1 parent eb8d49a commit ae40b80

3 files changed

Lines changed: 159 additions & 44 deletions

File tree

rssext.go

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"encoding/xml"
1010
"fmt"
1111
"io"
12+
"strings"
1213
"time"
1314
)
1415

@@ -19,28 +20,26 @@ type RssFeedExtXml struct {
1920
ContentNamespace string `xml:"xmlns:content,attr"`
2021
CustomNamespaces []xml.Attr `xml:",attr"`
2122
Channel *RssFeedExt
22-
}
23-
24-
type RssContentExt struct {
25-
XMLName xml.Name `xml:"content:encoded"`
26-
Content string `xml:",cdata"`
23+
Extensions []Extension `xml:",any"`
2724
}
2825

2926
type RssImageExt struct {
30-
XMLName xml.Name `xml:"image"`
31-
Url string `xml:"url"`
32-
Title string `xml:"title"`
33-
Link string `xml:"link"`
34-
Width int `xml:"width,omitempty"`
35-
Height int `xml:"height,omitempty"`
27+
XMLName xml.Name `xml:"image"`
28+
Url string `xml:"url"`
29+
Title string `xml:"title"`
30+
Link string `xml:"link"`
31+
Width int `xml:"width,omitempty"`
32+
Height int `xml:"height,omitempty"`
33+
Extensions []Extension `xml:",any"`
3634
}
3735

3836
type RssTextInputExt struct {
39-
XMLName xml.Name `xml:"textInput"`
40-
Title string `xml:"title"`
41-
Description string `xml:"description"`
42-
Name string `xml:"name"`
43-
Link string `xml:"link"`
37+
XMLName xml.Name `xml:"textInput"`
38+
Title string `xml:"title"`
39+
Description string `xml:"description"`
40+
Name string `xml:"name"`
41+
Link string `xml:"link"`
42+
Extensions []Extension `xml:",any"`
4443
}
4544

4645
type RssFeedExt struct {
@@ -65,36 +64,23 @@ type RssFeedExt struct {
6564
Image *RssImageExt
6665
TextInput *RssTextInputExt
6766
Items []*RssItemExt `xml:"item"`
67+
Extensions []Extension `xml:",any"`
6868
}
6969

7070
type RssItemExt struct {
7171
XMLName xml.Name `xml:"item"`
7272
Title string `xml:"title"` // required
7373
Link string `xml:"link"` // required
7474
Description string `xml:"description"` // required
75-
Content *RssContentExt
75+
Content *RssContent
7676
Author string `xml:"author,omitempty"`
7777
Category string `xml:"category,omitempty"`
7878
Comments string `xml:"comments,omitempty"`
79-
Enclosure *RssEnclosureExt
80-
Guid *RssGuidExt // Id used
79+
Enclosure *RssEnclosure
80+
Guid *RssGuid // Id used
8181
PubDate string `xml:"pubDate,omitempty"` // created or updated
8282
Source string `xml:"source,omitempty"`
83-
}
84-
85-
type RssEnclosureExt struct {
86-
//RSS 2.0 <enclosure url="http://example.com/file.mp3" length="123456789" type="audio/mpeg" />
87-
XMLName xml.Name `xml:"enclosure"`
88-
Url string `xml:"url,attr"`
89-
Length string `xml:"length,attr"`
90-
Type string `xml:"type,attr"`
91-
}
92-
93-
type RssGuidExt struct {
94-
//RSS 2.0 <guid isPermaLink="true">http://inessential.com/2002/09/01.php#a2</guid>
95-
XMLName xml.Name `xml:"guid"`
96-
Id string `xml:",chardata"`
97-
IsPermaLink string `xml:"isPermaLink,attr,omitempty"` // "true", "false", or an empty string
83+
Extensions []Extension `xml:",any"`
9884
}
9985

10086
type RssExt struct {
@@ -109,21 +95,21 @@ func newRssItemExt(i *Item) *RssItemExt {
10995
PubDate: anyTimeFormat(time.RFC1123Z, i.Created, i.Updated),
11096
}
11197
if i.Id != "" {
112-
item.Guid = &RssGuidExt{Id: i.Id, IsPermaLink: i.IsPermaLink}
98+
item.Guid = &RssGuid{Id: i.Id, IsPermaLink: i.IsPermaLink}
11399
}
114100
if i.Link != nil {
115101
item.Link = i.Link.Href
116102
}
117103
if len(i.Content) > 0 {
118-
item.Content = &RssContentExt{Content: i.Content}
104+
item.Content = &RssContent{Content: i.Content}
119105
}
120106
if i.Source != nil {
121107
item.Source = i.Source.Href
122108
}
123109

124110
// Define a closure
125111
if i.Enclosure != nil && i.Enclosure.Type != "" && i.Enclosure.Length != "" {
126-
item.Enclosure = &RssEnclosureExt{Url: i.Enclosure.Url, Type: i.Enclosure.Type, Length: i.Enclosure.Length}
112+
item.Enclosure = &RssEnclosure{Url: i.Enclosure.Url, Type: i.Enclosure.Type, Length: i.Enclosure.Length}
127113
}
128114

129115
if i.Author != nil {
@@ -182,10 +168,6 @@ func (r *RssFeedExt) FeedXml() interface{} {
182168
Version: "2.0",
183169
Channel: r,
184170
ContentNamespace: "http://purl.org/rss/1.0/modules/content/",
185-
CustomNamespaces: []xml.Attr{
186-
// TODO: we need more here ...
187-
{Name: xml.Name{Local: "xmlns:content"}, Value: "http://purl.org/rss/1.0/modules/content/"},
188-
},
189171
}
190172
}
191173

@@ -231,3 +213,34 @@ func UnmarshalRssFeedExt(data []byte) (*RssFeedExtXml, error) {
231213
}
232214
return feed, nil
233215
}
216+
217+
type Extension struct {
218+
XMLName xml.Name
219+
Attrs []xml.Attr `xml:",any,attr"`
220+
Children []Extension `xml:",any"`
221+
Value string `xml:",chardata"`
222+
}
223+
224+
func (e *Extension) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
225+
// Filter xmlns attributes
226+
var filteredAttrs []xml.Attr
227+
for _, attr := range start.Attr {
228+
if attr.Name.Space == "xmlns" || attr.Name.Local == "xmlns" {
229+
continue
230+
}
231+
filteredAttrs = append(filteredAttrs, attr)
232+
}
233+
// Update start.Attr so DecodeElement uses the filtered list
234+
start.Attr = filteredAttrs
235+
236+
type extensionAlias Extension
237+
var v extensionAlias
238+
239+
if err := d.DecodeElement(&v, &start); err != nil {
240+
return err
241+
}
242+
243+
*e = Extension(v)
244+
e.Value = strings.TrimSpace(e.Value)
245+
return nil
246+
}

rssext_test.go

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,46 @@ var testRssFeedExtXML = RssFeedExtXml{
4646
Title: "Lorem ipsum 2018-10-30T23:22:00+00:00",
4747
Link: "http://example.com/test/1540941720",
4848
Description: "Exercitation ut Lorem sint proident.",
49-
Content: (*RssContentExt)(nil),
49+
Content: (*RssContent)(nil),
5050
Author: "",
5151
Category: "",
5252
Comments: "",
53-
Enclosure: (*RssEnclosureExt)(nil),
54-
Guid: &RssGuidExt{XMLName: xml.Name{Local: "guid"}, Id: "http://example.com/test/1540941720", IsPermaLink: "true"},
53+
Enclosure: (*RssEnclosure)(nil),
54+
Guid: &RssGuid{XMLName: xml.Name{Local: "guid"}, Id: "http://example.com/test/1540941720", IsPermaLink: "true"},
5555
PubDate: "Tue, 30 Oct 2018 23:22:00 GMT",
5656
Source: "",
57+
Extensions: []Extension{
58+
{
59+
XMLName: xml.Name{Space: "http://purl.org/dc/elements/1.1/", Local: "creator"},
60+
Value: "John Smith",
61+
},
62+
{
63+
XMLName: xml.Name{Space: "http://www.example.com/custom", Local: "group"},
64+
Attrs: []xml.Attr{{Name: xml.Name{Local: "name"}, Value: "item name"}},
65+
Children: []Extension{
66+
{
67+
XMLName: xml.Name{Space: "http://www.example.com/custom", Local: "elem"},
68+
Value: "item content",
69+
},
70+
},
71+
},
72+
},
73+
},
74+
},
75+
Extensions: []Extension{
76+
{
77+
XMLName: xml.Name{Local: "author"},
78+
Value: "John Smith",
79+
},
80+
{
81+
XMLName: xml.Name{Space: "http://www.example.com/custom", Local: "group"},
82+
Attrs: []xml.Attr{{Name: xml.Name{Local: "name"}, Value: "channel name"}},
83+
Children: []Extension{
84+
{
85+
XMLName: xml.Name{Space: "http://www.example.com/custom", Local: "elem"},
86+
Value: "channel content",
87+
},
88+
},
5789
},
5890
},
5991
},
@@ -99,3 +131,63 @@ func TestRssExtRoundTrip(t *testing.T) {
99131
t.Error("Roundtrip failed: objects are different")
100132
}
101133
}
134+
135+
func TestRssExtCustomExtensions(t *testing.T) {
136+
ns := "http://www.example.com/custom"
137+
138+
findExt := func(exts []Extension, space, local string) *Extension {
139+
for i := range exts {
140+
if exts[i].XMLName.Space == space && exts[i].XMLName.Local == local {
141+
return &exts[i]
142+
}
143+
}
144+
return nil
145+
}
146+
147+
// Check Channel
148+
group := findExt(testRssFeedExtXML.Channel.Extensions, ns, "group")
149+
if group == nil {
150+
t.Fatal("Channel custom:group not found")
151+
}
152+
153+
elem := findExt(group.Children, ns, "elem")
154+
if elem == nil {
155+
t.Fatal("Channel custom:elem not found inside group")
156+
}
157+
158+
if elem.Value != "channel content" {
159+
t.Errorf("Channel custom:elem value mismatch. Got %q, want %q", elem.Value, "channel content")
160+
}
161+
162+
// Check Item
163+
if len(testRssFeedExtXML.Channel.Items) == 0 {
164+
t.Fatal("No items in channel")
165+
}
166+
item := testRssFeedExtXML.Channel.Items[0]
167+
168+
group = findExt(item.Extensions, ns, "group")
169+
if group == nil {
170+
t.Fatal("Item custom:group not found")
171+
}
172+
173+
// Check attribute
174+
foundAttr := false
175+
for _, attr := range group.Attrs {
176+
if attr.Name.Local == "name" && attr.Value == "item name" {
177+
foundAttr = true
178+
break
179+
}
180+
}
181+
if !foundAttr {
182+
t.Error("Item custom:group attribute 'name' with value 'item name' not found")
183+
}
184+
185+
elem = findExt(group.Children, ns, "elem")
186+
if elem == nil {
187+
t.Fatal("Item custom:elem not found inside group")
188+
}
189+
190+
if elem.Value != "item content" {
191+
t.Errorf("Item custom:elem value mismatch. Got %q, want %q", elem.Value, "item content")
192+
}
193+
}

testext.rss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,23 @@
1313
<pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate>
1414
<copyright><![CDATA[Michael Bertolacci, licensed under a Creative Commons Attribution 3.0 Unported License.]]></copyright>
1515
<ttl>60</ttl>
16+
<custom:group name="channel name">
17+
<custom:elem>
18+
channel content
19+
</custom:elem>
20+
</custom:group>
1621
<item>
1722
<title><![CDATA[Lorem ipsum 2018-10-30T23:22:00+00:00]]></title>
1823
<description><![CDATA[Exercitation ut Lorem sint proident.]]></description>
1924
<link>http://example.com/test/1540941720</link>
2025
<guid isPermaLink="true">http://example.com/test/1540941720</guid>
2126
<dc:creator><![CDATA[John Smith]]></dc:creator>
2227
<pubDate>Tue, 30 Oct 2018 23:22:00 GMT</pubDate>
28+
<custom:group name="item name">
29+
<custom:elem>
30+
item content
31+
</custom:elem>
32+
</custom:group>
2333
</item>
2434
</channel>
2535
</rss>

0 commit comments

Comments
 (0)