rss_feed_plus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ec579256585d8f4a9ddf84497d214779d61c605cbf443bd1e55e4f094204cdc5
4
+ data.tar.gz: 0bf2827e8bfab1aa2806578303ff04a570991c97a61673dc0edd8c9bb82c57e1
5
+ SHA512:
6
+ metadata.gz: 566a7978173b0d7527a6413d80f6f0a2245633c51b7dc2950346e0b1b81ba10d465f906887f5e0b1fa5b06870c657e7426f47a3ac7e1bd989aa739daf3dfac7f
7
+ data.tar.gz: 1f0c46d97d326cbb2aa65dced535beee715b58b9570e7ced3f2d4cb36d5d876ac25c8d16cd17884b383171cdcd0dae34927c90d20e40cc1f1952b17c4bba3f23
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,11 @@
1
+ require:
2
+ - rubocop-performance
3
+ - rubocop-rspec
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 2.6
7
+ NewCops: enable
8
+ Style/FrozenStringLiteralComment:
9
+ EnforcedStyle: never
10
+ Style/Documentation:
11
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-03-16
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 talaatmagdyx
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # rss_feed_plus
2
+
3
+ ## Introduction
4
+
5
+ **rss_feed_plus** is your go-to Ruby gem for effortlessly fetching and parsing RSS feeds. Whether you're building a news aggregator, content management system, or simply want to integrate RSS feeds into your application, **rss_feed_plus** simplifies the process, allowing you to easily retrieve and process RSS feed data from various sources.
6
+
7
+ ## Features
8
+
9
+ - **Effortless Parsing**: Fetch and parse RSS feeds with ease.
10
+ - **Customization: Tailor** parsing to fit your needs with customizable XML and URI parsers and timeout duration.
11
+ - **Seamless Integration**: Integrate with Ruby applications smoothly.
12
+
13
+ ## Installation
14
+
15
+ Getting started with **rss_feed_plus** is quick and easy. Simply add the gem to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'rss_feed_plus'
19
+ ```
20
+
21
+ Then, install the gem by running:
22
+
23
+ ```bash
24
+ bundle install
25
+ ```
26
+
27
+ Alternatively, you can install the gem directly using RubyGems:
28
+
29
+ ```bash
30
+ gem install rss_feed_plus
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ Here's a basic example of how to use **rss_feed_plus** to fetch and parse RSS feeds:
36
+
37
+ ```ruby
38
+ require 'rss_feed'
39
+ require 'nokogiri'
40
+
41
+ # Define your custom options
42
+ feed_urls = 'https://www.ruby-lang.org/en/feeds/news.rss'
43
+ xml_parser = Nokogiri
44
+ uri_parser = URI
45
+ timeout = 10
46
+
47
+ # Initialize the Parser class with custom options
48
+ parser = RssFeed::Parser.new(feed_urls, xml_parser: xml_parser, uri_parser: uri_parser, timeout: timeout)
49
+ # or
50
+ parser = RssFeed::Parser.new(feed_urls)
51
+ # Parse the RSS feeds
52
+ parsed_data = parser.parse_as_object
53
+
54
+ # Process the parsed data
55
+ puts parsed_data.inspect
56
+ ```
57
+
58
+ ## Customization
59
+
60
+ **rss_feed_plus** allows you to tailor the parsing process to fit your needs. Customize the XML parser, URI parser, and timeout duration according to your requirements.
61
+
62
+ ## Contributing
63
+
64
+ Contributions to **rss_feed_plus** are welcome! If you encounter any issues, have feature requests, or would like to contribute enhancements, please feel free to open an issue or submit a pull request on [GitHub](https://github.com/talaatmagdyx/rss_feed_plus).
65
+
66
+ Before contributing, please review the [Contributing Guidelines](https://github.com/talaatmagdyx/rss_feed_plus/blob/master/.github/CONTRIBUTING.md) and adhere to the [Code of Conduct](https://github.com/talaatmagdyx/rss_feed_plus/blob/master/.github/CODE_OF_CONDUCT.md).
67
+
68
+ ## Reporting Bugs / Feature Requests
69
+
70
+ If you encounter any bugs or have suggestions for new features, please [open an issue on GitHub](https://github.com/talaatmagdyx/rss_feed_plus/issues). Your feedback is valuable and helps improve the quality of the gem.
71
+
72
+ ## License
73
+
74
+ **rss_feed_plus** is released under the [MIT License](https://opensource.org/licenses/MIT). You are free to use, modify, and distribute the gem according to the terms of the license.
75
+
76
+ ## Code of Conduct
77
+
78
+ Please review and adhere to the [Code of Conduct](https://github.com/talaatmagdyx/rss_feed_plus/blob/master/.github/CODE_OF_CONDUCT.md) when interacting with the **rss_feed_plus** project. We strive to maintain a welcoming and inclusive community for all contributors and users.
79
+
80
+ ---
81
+
82
+ Experience the simplicity of RSS feed integration with **rss_feed_plus**. Happy coding!
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,58 @@
1
+ class DynamicObject
2
+ ## Example usage:
3
+ # data = {
4
+ # name: "John",
5
+ # age: 30,
6
+ # address: {
7
+ # city: "New York",
8
+ # state: "NY"
9
+ # },
10
+ # hobbies: ["reading", "hiking"]
11
+ # }
12
+ #
13
+ # dynamic_object = DynamicObject.new(data)
14
+ #
15
+ # puts dynamic_object.name # Output: John
16
+ # puts dynamic_object.age # Output: 30
17
+ # puts dynamic_object.address.city # Output: New York
18
+ # puts dynamic_object.address.state # Output: NY
19
+ # puts dynamic_object.hobbies # Output: ["reading", "hiking"]
20
+
21
+ # A class for initializing objects dynamically based on given data.
22
+ # Initializes a new instance of DynamicInitializer.
23
+ #
24
+ # @param data [Hash] The data used to initialize the object.
25
+ # @return [DynamicInitializer] A new instance of DynamicInitializer.
26
+ def initialize(data)
27
+ data.each do |key, value|
28
+ key = key.to_s
29
+ set_instance_variable(key, value)
30
+ define_singleton_method(key.tr(':', '_')) { instance_variable_get("@#{key.tr(':', '_')}") }
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # Sets an instance variable based on the provided key and value.
37
+ #
38
+ # @param key [String] The key for the instance variable.
39
+ # @param value [Object] The value to be assigned to the instance variable.
40
+ # @return [void]
41
+ def set_instance_variable(key, value)
42
+ if value.is_a?(Hash)
43
+ instance_variable_set("@#{key.tr(':', '_')}", DynamicObject.new(value))
44
+ elsif value.is_a?(Array)
45
+ instance_variable_set("@#{key}", process_array(value))
46
+ else
47
+ instance_variable_set("@#{key}", value)
48
+ end
49
+ end
50
+
51
+ # Processes an array, creating DynamicObject instances if elements are hashes.
52
+ #
53
+ # @param array [Array] The array to be processed.
54
+ # @return [Array] The processed array.
55
+ def process_array(array)
56
+ array.map { |v| v.is_a?(Hash) ? DynamicObject.new(v) : v }
57
+ end
58
+ end
@@ -0,0 +1,41 @@
1
+ module RssFeed
2
+ module Feed
3
+ # The Base class serves as the base class for all feed parsers.
4
+ class Base
5
+ attr_reader :document
6
+
7
+ # Initializes a new Base instance.
8
+ #
9
+ # @param document [Nokogiri::XML::Document] The parsed XML document.
10
+ def initialize(document)
11
+ @document = document
12
+ end
13
+
14
+ # This method should be implemented by subclasses to define the specific parsing logic.
15
+ #
16
+ # @abstract
17
+ def parser
18
+ raise NotImplementedError
19
+ end
20
+
21
+ private
22
+
23
+ # Detects the type of the feed based on the root element of the XML document.
24
+ #
25
+ # @return [String] The name of the root element.
26
+ def detect_feed_type
27
+ document.root.name
28
+ end
29
+
30
+ # Executes the appropriate parsing method based on the detected feed type.
31
+ #
32
+ # @raise [NotImplementedError] If the parsing method for the detected feed type is not implemented.
33
+ def execute_method
34
+ method_name = detect_feed_type
35
+ raise NotImplementedError unless respond_to?(method_name)
36
+
37
+ send(method_name)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,47 @@
1
+ require_relative 'base'
2
+ require_relative '../object'
3
+
4
+ module RssFeed
5
+ module Feed
6
+ # The Channel class represents a channel in an RSS feed.
7
+ class Channel < Base
8
+ # List of commonly used tags in an RSS channel.
9
+ TAGS = %w[
10
+ id
11
+ title subtitle link
12
+ description
13
+ author webMaster managingEditor contributor
14
+ pubDate lastBuildDate updated dc:date
15
+ generator language docs cloud
16
+ ttl skipHours skipDays
17
+ image logo icon rating
18
+ rights copyright
19
+ textInput feedburner:browserFriendly
20
+ itunes:author itunes:category
21
+ ].freeze
22
+
23
+ # XPath expression for selecting the RSS channel.
24
+ def rss
25
+ return nil if document.blank?
26
+
27
+ '//channel'
28
+ end
29
+
30
+ # XPath expression for selecting the Atom feed.
31
+ def atom
32
+ return nil if document.blank?
33
+
34
+ '//feed'
35
+ end
36
+
37
+ alias feed atom
38
+
39
+ # Parses the RSS channel or Atom feed based on the detected feed type.
40
+ #
41
+ # @return [Nokogiri::XML::NodeSet, nil] The parsed channel or feed, or nil if not found.
42
+ def parse
43
+ document.xpath(execute_method)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ require_relative 'base'
2
+
3
+ module RssFeed
4
+ module Feed
5
+ # The Item class represents an item in an RSS feed.
6
+ class Item < Base
7
+ # List of commonly used tags in an RSS item.
8
+ TAGS = %w[
9
+ id
10
+ title link link+alternate link+self link+edit link+replies
11
+ author contributor
12
+ description summary content content:encoded comments
13
+ pubDate published updated expirationDate modified dc:date
14
+ category guid
15
+ trackback:ping trackback:about
16
+ dc:creator dc:title dc:subject dc:rights dc:publisher
17
+ feedburner:origLink media:content media:thumbnail
18
+ media:title
19
+ media:credit
20
+ media:category
21
+ ].freeze
22
+
23
+ # XPath expression for selecting the RSS item.
24
+ def rss
25
+ '//item'
26
+ end
27
+
28
+ # XPath expression for selecting the Atom entry.
29
+ def atom
30
+ '//entry'
31
+ end
32
+
33
+ alias feed atom
34
+ # Parses the RSS item or Atom entry based on the detected feed type.
35
+ #
36
+ # @return [Nokogiri::XML::NodeSet] The parsed item or entry.
37
+ def parse
38
+ document.xpath(execute_method)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,75 @@
1
+ require 'cgi'
2
+ require_relative '../object'
3
+
4
+ module RssFeed
5
+ module Feed
6
+ # The Namespace module provides utility methods for accessing and manipulating XML namespaces in RSS feeds.
7
+ class Namespace
8
+ # Mapping of namespace prefixes to their corresponding URIs.
9
+ NAMESPACES = {
10
+ 'itunes' => 'http://www.itunes.com/dtds/podcast-1.0.dtd',
11
+ 'dc' => 'http://purl.org/dc/elements/1.1/',
12
+ 'feedburner' => 'http://rssnamespace.org/feedburner/ext/1.0',
13
+ 'content' => 'http://purl.org/rss/1.0/modules/content/',
14
+ 'trackback' => 'http://example.com/trackback',
15
+ 'media' => 'http://search.yahoo.com/mrss/'
16
+ }.freeze
17
+
18
+ class << self
19
+ # Accesses the specified XML tag within the document with proper namespace handling.
20
+ #
21
+ # @param tag [String] The XML tag to access.
22
+ # @param doc [Nokogiri::XML::Document] The XML document.
23
+ # @return [Hash] The tag data including text, nested elements flag, nested attributes flag, and the document.
24
+ def access_tag(tag, doc)
25
+ doc = doc.xpath(tag, namespace(tag))
26
+ nested_elements = nested_elements?(doc)
27
+ { text: doc.to_s, nested_elements: nested_elements, nested_attributes: nested_attributes?(doc), docs: doc }
28
+ end
29
+
30
+ # Resolves the namespace for the given XML tag.
31
+ #
32
+ # @param tag [String] The XML tag.
33
+ # @return [Hash] The namespace declaration.
34
+ def namespace(tag)
35
+ namespace_key = tag.split(':').first
36
+ { namespace_key.to_s => NAMESPACES[namespace_key] }.compact
37
+ end
38
+
39
+ # Removes HTML tags from the given content.
40
+ #
41
+ # @param content [String] The content containing HTML tags.
42
+ # @return [String] The content without HTML tags.
43
+ def remove_html_tags(content)
44
+ if %r{([^-_.!~*'()a-zA-Z\d;/?:@&=+$,\[\]]%)}.match?(content)
45
+ CGI.unescape(content)
46
+ else
47
+ content
48
+ end.gsub(/(<!\[CDATA\[|\]\]>)/, '').strip.gsub(/<[^>]+>/, '')
49
+ end
50
+
51
+ # Checks if the XML node has nested elements.
52
+ #
53
+ # @param node [Nokogiri::XML::NodeSet] The XML node.
54
+ # @return [Boolean] Whether the node has nested elements.
55
+ def nested_elements?(node)
56
+ return false if node.blank? || node.to_s == 'NaN'
57
+
58
+ return true if node.children.any?(&:element?)
59
+
60
+ false
61
+ end
62
+
63
+ # Checks if the XML node has nested attributes.
64
+ #
65
+ # @param node [Nokogiri::XML::NodeSet] The XML node.
66
+ # @return [Boolean] Whether the node has nested attributes.
67
+ def nested_attributes?(node)
68
+ return false if node.blank? || node.to_s == 'NaN'
69
+
70
+ node.any? { |thumbnail| !thumbnail.attributes.empty? }
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,47 @@
1
+ # Monkey-patching the Object class to add convenience methods for checking presence or absence of content.
2
+ class Object
3
+ # An object is present if it's not blank.
4
+ #
5
+ # @return [true, false].
6
+ def present?
7
+ !blank?
8
+ end
9
+
10
+ # An object is blank if it's false, empty, or a whitespace string.
11
+ # For example, +nil+, '', ' ', [], {}, and +false+ are all blank.
12
+ #
13
+ # This simplifies
14
+ #
15
+ # !address || address.empty?
16
+ #
17
+ # to
18
+ #
19
+ # address.blank?
20
+ #
21
+ # @return [true, false]
22
+ def blank?
23
+ # If the object responds to `empty?`, checks if it's empty or equals 'NaN'.
24
+ # Otherwise, checks if the object is nil.
25
+ respond_to?(:empty?) ? (empty? || self == 'NaN') : !self
26
+ end
27
+
28
+ # Returns the receiver if it's present otherwise returns +nil+.
29
+ # <tt>object.presence</tt> is equivalent to
30
+ #
31
+ # object.present? ? object : nil
32
+ #
33
+ # For example, something like
34
+ #
35
+ # state = params[:state] if params[:state].present?
36
+ # country = params[:country] if params[:country].present?
37
+ # region = state || country || 'US'
38
+ #
39
+ # becomes
40
+ #
41
+ # region = params[:state].presence || params[:country].presence || 'US'
42
+ #
43
+ # @return [Object]
44
+ def presence
45
+ self if present?
46
+ end
47
+ end
@@ -0,0 +1,209 @@
1
+ require 'nokogiri'
2
+ require 'open-uri'
3
+ require 'socket'
4
+
5
+ require_relative 'feed/channel'
6
+ require_relative 'feed/item'
7
+ require_relative 'feed/namespace'
8
+ require_relative '../rss_feed/object'
9
+ require_relative '../rss_feed/dynamic_object'
10
+
11
+ module RssFeed
12
+ # The Parser class is responsible for parsing RSS feeds.
13
+ class Parser
14
+ attr_reader :feed_urls, :xml_parser, :uri_parser
15
+
16
+ # Initialize the Parser with a list of feed URLs.
17
+ #
18
+ # @param feed_urls String The URLs of the RSS feeds to parse.
19
+ def initialize(feed_urls, options = {})
20
+ @feed_urls = feed_urls
21
+ @xml_parser = options.fetch(:xml_parser, Nokogiri)
22
+ @uri_parser = options.fetch(:uri_parser, URI)
23
+ @timeout = options.fetch(:timeout, 10) # Default timeout: 10 seconds
24
+ @logger = options[:logger]
25
+ end
26
+
27
+ # Parse the RSS feeds and extract channel and item information.
28
+ #
29
+ # @return [Hash] The parsed channel and item information.
30
+ def parse
31
+ document = fetch_and_parse_xml(feed_urls)
32
+ channel = RssFeed::Feed::Channel.new(document)
33
+ channel_info = extract_channel_info(channel)
34
+
35
+ items = RssFeed::Feed::Item.new(document)
36
+ item_info = extract_item_info(items)
37
+
38
+ { 'channel' => channel_info, 'items' => item_info }
39
+ end
40
+
41
+ def parse_as_object
42
+ DynamicObject.new(parse)
43
+ end
44
+
45
+ private
46
+
47
+ # Fetch and parse XML data from the given URL.
48
+ #
49
+ # @param url [String] The URL of the XML data.
50
+ # @return [Nokogiri::XML::Document] The parsed XML document.
51
+ # def fetch_and_parse_xml(url)
52
+ # rss_data = uri_parser.parse(url).open
53
+ # @xml_parser::XML(rss_data)
54
+ # rescue StandardError => e
55
+ # handle_error(e)
56
+ # raise RssFetchError, "Failed to fetch or parse XML: #{e.message}"
57
+ # end
58
+ def fetch_and_parse_xml(url)
59
+ rss_data = URI.parse(url).open(**uri_options)
60
+ @xml_parser::XML(rss_data)
61
+ rescue SocketError, URI::InvalidURIError => e
62
+ raise RssFetchError, "Failed to fetch or parse XML: #{e.message}"
63
+ rescue Timeout::Error => e
64
+ raise RssFetchError, "HTTP request timed out: #{e.message}"
65
+ end
66
+
67
+ def uri_options
68
+ { open_timeout: @timeout, read_timeout: @timeout }.compact
69
+ end
70
+
71
+ # Extract channel information from the parsed XML document.
72
+ #
73
+ # @param channel [RssFeed::Feed::Channel] The channel object.
74
+ # @return [Hash] The extracted channel information.
75
+ def extract_channel_info(channel)
76
+ extract_info(channel, channel.parse)
77
+ end
78
+
79
+ # Extract item information from the parsed XML document.
80
+ #
81
+ # @param items [RssFeed::Feed::Item] The items object.
82
+ # @return [Array<Hash>] The extracted item information.
83
+ def extract_item_info(items)
84
+ items.parse.map { |item| extract_info(items, item) }
85
+ end
86
+
87
+ # Extract information from the XML document based on specified tags.
88
+ #
89
+ # @param feed [RssFeed::Feed::Channel/RssFeed::Feed::Item] The feed object.
90
+ # @param feed_parse [Hash] The parsed XML data.
91
+ # @return [Hash] The extracted information.
92
+ def extract_info(feed, feed_parse)
93
+ item_data = {}
94
+
95
+ feed.class::TAGS.each do |tag|
96
+ tag_data = extract_tag_data(tag, feed_parse)
97
+ next if skip_extraction?(tag_data)
98
+
99
+ items = extract_items(tag_data)
100
+ next if skip_items?(items, tag_data[:nested_attributes])
101
+
102
+ item_data[tag] = create_item_info(items, tag_data)
103
+ end
104
+
105
+ item_data
106
+ end
107
+
108
+ # Check if extraction of tag data should be skipped.
109
+ #
110
+ # @param tag_data [Hash] The tag data.
111
+ # @return [Boolean] True if extraction should be skipped, otherwise false.
112
+ def skip_extraction?(tag_data)
113
+ tag_data.values_at(:text, :nested_elements, :nested_attributes).all?(&:blank?)
114
+ end
115
+
116
+ # Check if extraction of items should be skipped.
117
+ #
118
+ # @param items [Object] The items to check.
119
+ # @param nested_attributes [Boolean] Whether the items have nested attributes.
120
+ # @return [Boolean] True if extraction should be skipped, otherwise false.
121
+ def skip_items?(items, nested_attributes)
122
+ items.blank? && nested_attributes.blank?
123
+ end
124
+
125
+ # Create item information hash.
126
+ #
127
+ # @param items [Object] The items data.
128
+ # @param tag_data [Hash] The tag data.
129
+ # @return [Hash] The item information.
130
+ def create_item_info(items, tag_data)
131
+ { 'values' => items, 'attributes' => tag_data[:attributes] }.compact
132
+ end
133
+
134
+ # Extract tag data from the XML document.
135
+ #
136
+ # @param tag [String] The tag to extract.
137
+ # @param feed_parse [Hash] The parsed XML data.
138
+ # @return [Hash] The extracted tag data.
139
+ def extract_tag_data(tag, feed_parse)
140
+ value = RssFeed::Feed::Namespace.access_tag(tag, feed_parse)
141
+ value[:attributes] = extract_attributes(value[:docs]) if value[:nested_attributes]
142
+ value
143
+ end
144
+
145
+ # Extract items from the XML document.
146
+ #
147
+ # @param tag_data [Hash] The tag data.
148
+ # @return [Object] The extracted items.
149
+ def extract_items(tag_data)
150
+ tag_data[:nested_elements] ? extract_nested_data(tag_data[:docs]) : extract_clean_value(tag_data[:text])
151
+ end
152
+
153
+ # Add attributes to the item information hash.
154
+ #
155
+ # @param tag_item [Hash] The item information hash.
156
+ # @param tag_data [Hash] The tag data.
157
+ def add_attributes(tag_item, tag_data)
158
+ tag_item['attributes'] = tag_data[:attributes] if tag_data[:attributes].present?
159
+ end
160
+
161
+ # Extract clean value from the XML document.
162
+ #
163
+ # @param docs [Object] The XML document.
164
+ # @return [String] The extracted clean value.
165
+ def extract_clean_value(docs)
166
+ RssFeed::Feed::Namespace.remove_html_tags(docs).presence
167
+ end
168
+
169
+ # Extract nested data from the XML document.
170
+ #
171
+ # @param nodes [Object] The XML nodes.
172
+ # @return [Hash] The extracted nested data.
173
+ def extract_nested_data(nodes)
174
+ nodes.each_with_object({}) do |node, nested_data|
175
+ node.children.each do |child|
176
+ child_value = RssFeed::Feed::Namespace.remove_html_tags(child.text)
177
+ nested_data[child.name.to_sym] = child_value if child_value.present?
178
+ end
179
+ end
180
+ end
181
+
182
+ # Extract attributes from the XML document.
183
+ #
184
+ # @param node [Object] The XML node.
185
+ # @return [Array<Hash>] The extracted attributes.
186
+ def extract_attributes(node)
187
+ node.map do |thumbnail|
188
+ attributes_hash = {}
189
+ thumbnail.attributes.each do |name, value|
190
+ attributes_hash[name.to_s] = value.to_s
191
+ end
192
+ attributes_hash
193
+ end
194
+ end
195
+
196
+ def handle_error(error)
197
+ error_message = "Error occurred: #{error.message}"
198
+ # Fallback to puts if logger is not configured
199
+ @logger.present? ? @logger.error(error_message) : puts(error_message)
200
+ end
201
+
202
+ def configure_logger
203
+ @logger ||= Logger.new($stdout)
204
+ @logger.level = Logger::INFO
205
+ end
206
+ end
207
+
208
+ class RssFetchError < StandardError; end
209
+ end
@@ -0,0 +1,3 @@
1
+ module RssFeed
2
+ VERSION = '0.1.0'.freeze
3
+ end
data/lib/rss_feed.rb ADDED
@@ -0,0 +1,5 @@
1
+ require_relative 'rss_feed/version'
2
+
3
+ module RssFeed
4
+ autoload :Parser, 'rss_feed/parser'
5
+ end