diff --git a/src/Attributes/BasePropertyAttribute.cs b/src/Attributes/BasePropertyAttribute.cs index 1f104b2..2a82c7c 100644 --- a/src/Attributes/BasePropertyAttribute.cs +++ b/src/Attributes/BasePropertyAttribute.cs @@ -13,14 +13,14 @@ public abstract class BasePropertyAttribute : Attribute public string Namespace { get; set; } - public Uri Url { get; private set; } + public Uri Uri { get; private set; } public string AttributeName { get; set; } - public string NamespaceUrl + public string NamespaceUri { - get => Url?.ToString(); - set => Url = new Uri(value); + get => Uri?.ToString(); + set => Uri = new Uri(value); } public Type Writer { get; set; } diff --git a/src/Attributes/XmlNamespaceAttribute.cs b/src/Attributes/XmlNamespaceAttribute.cs new file mode 100644 index 0000000..274cac8 --- /dev/null +++ b/src/Attributes/XmlNamespaceAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace dng.Syndication.Attributes +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] + public class XmlNamespaceAttribute : Attribute + { + public string Prefix { get; } + public Uri Uri { get; } + + public XmlNamespaceAttribute(string prefix, string uri) + { + Prefix = prefix; + Uri = new Uri(uri); + } + } +} \ No newline at end of file diff --git a/src/Feed.cs b/src/Feed.cs index 3749bc2..866d6ef 100644 --- a/src/Feed.cs +++ b/src/Feed.cs @@ -9,8 +9,6 @@ namespace dng.Syndication { public class Feed : IFeed { - - /// /// Name for the feed. /// @@ -29,8 +27,8 @@ public class Feed : IFeed /// Defines the hyperlink to the channel /// [AtomProperty("id", Required = true)] - [AtomProperty("link", NamespaceUrl = Namespaces.AtomNamespace, Writer = typeof(AtomLinkElementWriter))] - [Rss20Property("link", NamespaceUrl = Namespaces.AtomNamespace, Writer = typeof(AtomLinkElementWriter))] + [AtomProperty("link", NamespaceUri = Namespaces.AtomNamespace, Writer = typeof(AtomLinkElementWriter))] + [Rss20Property("link", NamespaceUri = Namespaces.AtomNamespace, Writer = typeof(AtomLinkElementWriter))] [Rss20Property("link", Required = true, Writer = typeof(Rcf3986UriElementFormatter))] public Uri Link { get; set; } diff --git a/src/Generators/AtomGenerator.cs b/src/Generators/AtomGenerator.cs index 5f3a4d0..b8a70e5 100644 --- a/src/Generators/AtomGenerator.cs +++ b/src/Generators/AtomGenerator.cs @@ -1,4 +1,7 @@ -using System.Text; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; using System.Xml.Linq; using dng.Syndication.Attributes; using dng.Syndication.Enums; @@ -18,6 +21,8 @@ public override string Process() var rootElement = new XElement(atomNamespace + "feed"); + AddFeedNamespaces(rootElement); + var declaration = new XDeclaration("1.0", Encoding.UTF8.HeaderName, null); var doc = new XDocument(declaration, rootElement); diff --git a/src/Generators/Generator.cs b/src/Generators/Generator.cs index 83bf793..ddcb94d 100644 --- a/src/Generators/Generator.cs +++ b/src/Generators/Generator.cs @@ -34,13 +34,30 @@ public override string ToString() public abstract FeedType FeedType { get; } - protected virtual XElement ParseProperties(TObject obj, XElement parentElement = null, XNamespace @namespace = null) + protected virtual void AddFeedNamespaces(XElement element) + { + var namespaceAttributes = + Feed.GetType().GetCustomAttributes(typeof(XmlNamespaceAttribute), false) + .Cast().ToList(); + + if (!namespaceAttributes.Any()) + { + return; + } + + foreach (var attribute in namespaceAttributes) + { + element.SetAttributeValue(XNamespace.Xmlns + attribute.Prefix, attribute.Uri); + } + } + + protected virtual XElement ParseProperties(TObject obj, XElement parentElement, XNamespace @namespace = null) where TObject : class { return ParsePropertiesInternal(obj, (object) null, parentElement, @namespace); } - internal XElement ParsePropertiesInternal(TObject obj, TParent parentobj, XElement parentElement = null, XNamespace @namespace = null) + internal XElement ParsePropertiesInternal(TObject obj, TParent parentobj, XElement parentElement, XNamespace @namespace = null) where TObject : class where TParent : class { @@ -52,7 +69,6 @@ internal XElement ParsePropertiesInternal(TObject obj, TParent property.GetCustomAttributes(typeof(TAttribute), false) .Cast().ToList(); - if (!propertyAttributes.Any()) { continue; @@ -150,9 +166,9 @@ private static XName GetElementName(TAttribute attribute, XNamespace @namespace) { XName result = attribute.Name; - if (!string.IsNullOrWhiteSpace(attribute.NamespaceUrl)) + if (!string.IsNullOrWhiteSpace(attribute.NamespaceUri)) { - result = XNamespace.Get(attribute.NamespaceUrl) + attribute.Name; + result = XNamespace.Get(attribute.NamespaceUri) + attribute.Name; } else if (@namespace != null) { diff --git a/src/Generators/Rss20Generator.cs b/src/Generators/Rss20Generator.cs index b5c7914..11cc1aa 100644 --- a/src/Generators/Rss20Generator.cs +++ b/src/Generators/Rss20Generator.cs @@ -26,6 +26,8 @@ public override string Process() rootElement.Add(new XAttribute(XNamespace.Xmlns + "atom", atomNamespace)); + AddFeedNamespaces(rootElement); + var parentElement = new XElement("channel"); rootElement.Add(ParseProperties(Feed, parentElement)); diff --git a/tests/AtomGeneratorTests.cs b/tests/AtomGeneratorTests.cs index 2abeab5..75120b7 100644 --- a/tests/AtomGeneratorTests.cs +++ b/tests/AtomGeneratorTests.cs @@ -1,13 +1,24 @@ using System; using System.Collections.Generic; using System.Globalization; +using dng.Syndication.Attributes; using dng.Syndication.Generators; +using dng.Syndication.Writers; using Xunit; namespace dng.Syndication.Tests { public class AtomGeneratorTests { + private const string DOTNETGEEK_ATOM_NAMESPACE = "http://www.dotnetgeek.com/Atom/2018"; + + [XmlNamespace("dng", DOTNETGEEK_ATOM_NAMESPACE)] + private class CustomAtomFeed : Feed + { + [AtomProperty("property", NamespaceUri = DOTNETGEEK_ATOM_NAMESPACE)] + public string MyProperty { get; set; } + } + private Feed CreateSimpleFeed() { var feed = new Feed @@ -38,6 +49,39 @@ private Feed CreateSimpleFeed() } }; + return feed; + } + + private CustomAtomFeed CreateCustomFeed() + { + var feed = new CustomAtomFeed + { + Title = FeedContent.Text("dotnetgeek feed"), + Author = new Author + { + Name = "Daniel", + Email = "email@email.em" + }, + Copyright = "2016 @ www.dotnetgeek.com", + Description = FeedContent.Text("Dotnet relevant thinks"), + Generator = "dng.Syndication", + Language = CultureInfo.GetCultureInfo("de"), + UpdatedDate = new DateTime(2016, 08, 16), + Link = new Uri("http://www.dotnetgeek.de/rss"), + FeedEntries = new List + { + new FeedEntry + { + Title = FeedContent.Plain("First Entry"), + Content = FeedContent.Plain("Content"), + Link = new Uri("http://www.dotnetgeek.com/first-entry"), + Summary = FeedContent.Plain("summary"), + PublishDate = new DateTime(2016, 08, 16), + Updated = new DateTime(2016, 08, 16), + } + }, + MyProperty = "my_custom_atom_value" + }; return feed; } @@ -107,6 +151,38 @@ public void Create_a_simple_atom_feed_with_logo() Assert.Equal(expected, feedXml); } + + [Fact] + public void Create_a_custom_atom_feed() + { + var atomGenerator = new AtomGenerator(CreateCustomFeed()); + var feedXml = atomGenerator.Process(); + + const string expected = "" + + "" + + "dotnetgeek feed" + + "Danielemail@email.em" + + "http://www.dotnetgeek.de/rss" + + "" + + "2016-08-16T00:00:00Z" + + "2016 @ www.dotnetgeek.com" + + "dng.Syndication" + + "Dotnet relevant thinks" + + "" + + "First Entry" + + "" + + "http://www.dotnetgeek.com/first-entry" + + "summary" + + "Content" + + "Danielemail@email.em" + + "2016-08-16T00:00:00Z" + + "2016-08-16T00:00:00Z" + + "" + + "my_custom_atom_value" + + ""; + + Assert.Equal(expected, feedXml); + } } } diff --git a/tests/Rss20GeneratorTests.cs b/tests/Rss20GeneratorTests.cs index 8887255..90dc881 100644 --- a/tests/Rss20GeneratorTests.cs +++ b/tests/Rss20GeneratorTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using dng.Syndication.Attributes; using dng.Syndication.Generators; using Xunit; @@ -10,6 +11,15 @@ public class Rss20GeneratorTests { private readonly string _dateTimeOffset = new DateTime(2016, 08, 16).ToString("zzzz").Replace(":", ""); + private const string DOTNETGEEK_RSS_NAMESPACE = "http://www.dotnetgeek.com/Rss/2018"; + + [XmlNamespace("dng", DOTNETGEEK_RSS_NAMESPACE)] + private class CustomRssFeed : Feed + { + [Rss20Property("property", NamespaceUri = DOTNETGEEK_RSS_NAMESPACE)] + public string MyProperty { get; set; } + } + private Feed CreateSimpleFeed() { var feed = new Feed @@ -36,6 +46,35 @@ private Feed CreateSimpleFeed() } }; + return feed; + } + + private CustomRssFeed CreateCustomFeed() + { + var feed = new CustomRssFeed + { + Title = FeedContent.Plain("dotnetgeek feed"), + Author = new Author("Daniel", "email@email.em"), + Copyright = "2016 @ www.dotnetgeek.com", + Description = FeedContent.Plain("Dotnet relevant topics"), + Generator = "dng.Syndication", + Language = CultureInfo.GetCultureInfo("de"), + UpdatedDate = new DateTime(2016, 08, 16), + Link = new Uri("http://www.dotnetgeek.de/rss"), + FeedEntries = new List + { + new FeedEntry + { + Title = FeedContent.Plain("First Entry"), + Content = FeedContent.Plain("Content"), + Link = new Uri("http://www.dotnetgeek.com/first-entry"), + Summary = FeedContent.Plain("summary"), + PublishDate = new DateTime(2016, 08, 16), + Updated = new DateTime(2016, 08, 16) + } + }, + MyProperty = "my_custom_rss_value" + }; return feed; } @@ -103,8 +142,35 @@ public void Create_a_feed_with_image() Assert.Equal(expected, feedXml); } - - + + [Fact] + public void Create_a_custom_feed() + { + var rss20Generator = new Rss20Generator(CreateCustomFeed()); + var feedXml = rss20Generator.Process(); + + var expected = "" + + "" + + "dotnetgeek feed" + + "" + + "http://www.dotnetgeek.de/rss" + + $"Tue, 16 Aug 2016 00:00:00 {_dateTimeOffset}" + + "de" + + "2016 @ www.dotnetgeek.com" + + "dng.Syndication" + + "Dotnet relevant topics" + + "" + + "First Entry" + + "http://www.dotnetgeek.com/first-entry" + + "http://www.dotnetgeek.com/first-entry" + + "Content" + + $"Tue, 16 Aug 2016 00:00:00 {_dateTimeOffset}" + + "" + + "my_custom_rss_value" + + ""; + + Assert.Equal(expected, feedXml); + } } }