diff --git a/.gitignore b/.gitignore index 4d2a4d6..e284d7a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ pubspec.lock # Directory created by dartdoc # If you don't generate documentation locally you can remove this line. doc/api/ + +.idea +.dart_tool diff --git a/lib/client.dart b/lib/client.dart new file mode 100644 index 0000000..fba07b9 --- /dev/null +++ b/lib/client.dart @@ -0,0 +1,25 @@ +import 'dart:async'; + +import 'package:http/http.dart' as http; +import 'package:webfeedclient/domain/channel.dart'; +import 'package:xml/xml.dart' as xml; + +class WebFeedClient { + http.Client client; + + WebFeedClient() { + client = new http.Client(); + } + + Future fetch(String url) { + return client.get(url).then((response) { + print("Response Status Code: ${response.statusCode}"); + print("Response Body: ${response.body}"); + return response.body; + }).then((bodyString) { + var document = xml.parse(bodyString); + var channel = new Channel.parse(document); + return channel; + }); + } +} diff --git a/lib/domain/channel.dart b/lib/domain/channel.dart new file mode 100644 index 0000000..80f0e3d --- /dev/null +++ b/lib/domain/channel.dart @@ -0,0 +1,70 @@ +import 'dart:core'; + +import 'package:webfeedclient/domain/item.dart'; +import 'package:webfeedclient/util/helpers.dart'; +import 'package:xml/xml.dart'; + +import 'image.dart'; + +class Channel { + final String title; + final String description; + final String link; + final List items; + + final Image image; + final String lastBuildDate; + final String language; + final String generator; + final String copyright; + + Channel(this.title, this.description, this.link, this.items, + {this.image, this.lastBuildDate, this.language, this.generator, this.copyright}); + + factory Channel.parse(XmlDocument document) { + XmlElement channelElement; + try { + channelElement = document + .findAllElements("channel") + .first; + } on StateError { + throw new ArgumentError("channel not found"); + } + var title = xmlGetString(channelElement, "title"); + var description = xmlGetString(channelElement, "description"); + var link = xmlGetString(channelElement, "link"); + + var feeds = channelElement.findAllElements("item").map((XmlElement element) { + return new Item.parse(element); + }).toList(); + + Image image; + try { + image = new Image.parse(channelElement + .findElements("image") + .first); + } on StateError {} + + var lastBuildDate = xmlGetString(channelElement, "lastBuildDate", strict: false); + var language = xmlGetString(channelElement, "language", strict: false); + var generator = xmlGetString(channelElement, "generator", strict: false); + var copyright = xmlGetString(channelElement, "copyright", strict: false); + + return new Channel(title, description, link, feeds, image: image, + lastBuildDate: lastBuildDate, + language: language, + generator: generator, + copyright: copyright); + } + + @override + String toString() { + return ''' + title: $title + description: $description + link: $link + lastBuildDate: $lastBuildDate + items: $items + '''; + } +} diff --git a/lib/domain/image.dart b/lib/domain/image.dart new file mode 100644 index 0000000..8b31723 --- /dev/null +++ b/lib/domain/image.dart @@ -0,0 +1,27 @@ +import 'package:webfeedclient/util/helpers.dart'; +import 'package:xml/xml.dart'; + +class Image { + final String title; + final String url; + final String link; + + Image(this.title, this.url, this.link); + + factory Image.parse(XmlElement element) { + var title = xmlGetString(element, "title", strict: false); + var url = xmlGetString(element, "url", strict: false); + var link = xmlGetString(element, "link", strict: false); + + return new Image(title, url, link); + } + + @override + String toString() { + return ''' + title: $title + url: $url + link: $link + '''; + } +} diff --git a/lib/domain/item.dart b/lib/domain/item.dart new file mode 100644 index 0000000..9363f85 --- /dev/null +++ b/lib/domain/item.dart @@ -0,0 +1,33 @@ +import 'package:webfeedclient/util/helpers.dart'; +import 'package:xml/xml.dart'; + +class Item { + final String title; + final String description; + final String link; + final String guid; + final String pubDate; + + Item(this.title, this.description, this.link, {this.guid, this.pubDate}); + + factory Item.parse(XmlElement element) { + var title = xmlGetString(element, "title"); + var description = xmlGetString(element, "description"); + var link = xmlGetString(element, "link"); + + var guid = xmlGetString(element, "guid", strict: false); + var pubDate = xmlGetString(element, "pubDate", strict: false); + + return new Item(title, description, link, guid: guid, pubDate: pubDate); + } + + @override + String toString() { + return ''' + title: $title + description: $description + link: $link + pubDate: $pubDate + '''; + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..74965ab --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,6 @@ +import 'package:webfeedclient/client.dart'; + +void main() { + var client = new WebFeedClient(); + client.fetch("https://developer.apple.com/news/releases/rss/releases.rss"); +} \ No newline at end of file diff --git a/lib/util/helpers.dart b/lib/util/helpers.dart new file mode 100644 index 0000000..bca963c --- /dev/null +++ b/lib/util/helpers.dart @@ -0,0 +1,17 @@ +import 'dart:core'; + +import 'package:xml/xml.dart'; + +String xmlGetString(XmlElement element, String name, {strict: true}) { + try { + return element + .findElements(name) + .first + .text; + } on StateError { + if (strict) { + throw new ArgumentError("$name not found"); + } + } + return null; +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..ebbfd28 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,6 @@ +name: webfeedclient +dependencies: + xml: "^3.0.0" + http: "^0.11.3+16" +dev_dependencies: + test: ^0.12.0 \ No newline at end of file diff --git a/test/examples/Invalid.xml b/test/examples/Invalid.xml new file mode 100644 index 0000000..ce8ae0f --- /dev/null +++ b/test/examples/Invalid.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/test/examples/RSS.xml b/test/examples/RSS.xml new file mode 100644 index 0000000..97a6496 --- /dev/null +++ b/test/examples/RSS.xml @@ -0,0 +1,31 @@ + + + + News - Foo bar News + https://foo.bar.news/ + Foo bar News and Updates feed provided by Foo bar, Inc. + + https://foo.bar.news/logo.gif + Foo bar News + https://foo.bar.news/ + + en-US + Mon, 26 Mar 2018 14:00:00 PDT + Custom + Copyright 2018, Foo bar Inc. + + The standard Lorem Ipsum passage, used since the 1500s + https://foo.bar.news/1 + https://foo.bar.news/1?guid + Lorem ipsum dolor sit amet, consectetur adipiscing elit + Mon, 26 Mar 2018 14:00:00 PDT + + + Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC + https://foo.bar.news/2 + https://foo.bar.news/2?guid + Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium + Tue, 20 Mar 2018 10:00:00 PDT + + + \ No newline at end of file diff --git a/test/rss.dart b/test/rss.dart new file mode 100644 index 0000000..6d6cd6c --- /dev/null +++ b/test/rss.dart @@ -0,0 +1,43 @@ +import 'dart:io'; +import 'dart:core'; +import 'package:test/test.dart'; +import 'package:webfeedclient/domain/channel.dart'; +import 'package:xml/xml.dart' as xml; + +void main() { + test("parsing Invalid.xml", () { + var xmlString = new File("test/examples/Invalid.xml").readAsStringSync(); + var doc = xml.parse(xmlString); + + try { + new Channel.parse(doc); + fail("Should throw Argument Error"); + } on ArgumentError {} + }); + test("parsing RSS.xml", () { + var xmlString = new File("test/examples/RSS.xml").readAsStringSync(); + var doc = xml.parse(xmlString); + + var channel = new Channel.parse(doc); + + expect(channel.title, "News - Foo bar News"); + expect(channel.description, "Foo bar News and Updates feed provided by Foo bar, Inc."); + expect(channel.link, "https://foo.bar.news/"); + expect(channel.language, "en-US"); + expect(channel.lastBuildDate, "Mon, 26 Mar 2018 14:00:00 PDT"); + expect(channel.generator, "Custom"); + expect(channel.copyright, "Copyright 2018, Foo bar Inc."); + + expect(channel.image.title, "Foo bar News"); + expect(channel.image.url, "https://foo.bar.news/logo.gif"); + expect(channel.image.link, "https://foo.bar.news/"); + + expect(channel.items.length, 2); + + expect(channel.items.first.title, "The standard Lorem Ipsum passage, used since the 1500s"); + expect(channel.items.first.description, "Lorem ipsum dolor sit amet, consectetur adipiscing elit"); + expect(channel.items.first.link, "https://foo.bar.news/1"); + expect(channel.items.first.guid, "https://foo.bar.news/1?guid"); + expect(channel.items.first.pubDate, "Mon, 26 Mar 2018 14:00:00 PDT"); + }); +}