Class: Html2rss::Channel
- Inherits:
-
Data
- Object
- Data
- Html2rss::Channel
- Defined in:
- lib/html2rss/channel.rb
Overview
Materialized channel metadata for feed builders and FeedResult.
Extract once via Channel.from_response; builders consume this value object and do not own channel extraction. Safe to Marshal (no Response / Nokogiri retained). Strings and image URL are normalized and frozen at construction (including Marshal load).
Instance Attribute Summary collapse
- #author ⇒ String? readonly
- #description ⇒ String readonly
- #image ⇒ Html2rss::Url? readonly
- #language ⇒ String? readonly
- #last_build_date ⇒ Time readonly
- #title ⇒ String readonly
- #ttl ⇒ Integer readonly
- #url ⇒ Html2rss::Url readonly
Class Method Summary collapse
-
.from_response(response, overrides: {}) ⇒ Html2rss::Channel
Materializes channel attributes from an HTTP response and optional overrides.
Instance Method Summary collapse
-
#initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) ⇒ Channel
constructor
rubocop:disable Metrics/ParameterLists -- Data.define members are the channel contract.
Constructor Details
#initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) ⇒ Channel
rubocop:disable Metrics/ParameterLists -- Data.define members are the channel contract
159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/html2rss/channel.rb', line 159 def initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) super( title: freeze_string(title), url: url.is_a?(Url) ? url : Url.from_absolute(url), description: freeze_string(description), language: language && freeze_string(language), ttl: Integer(ttl), last_build_date: normalize_last_build_date(last_build_date), image: normalize_image(image), author: && freeze_string() ) end |
Instance Attribute Details
#author ⇒ String? (readonly)
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/html2rss/channel.rb', line 30 Channel = Data.define(:title, :url, :description, :language, :ttl, :last_build_date, :image, :author) do class << self ## # Materializes channel attributes from an HTTP response and optional overrides. # # @param response [Html2rss::RequestService::Response] # @param overrides [Hash{Symbol => Object}] optional overrides for channel attributes # @return [Html2rss::Channel] def from_response(response, overrides: {}) new( title: title_for(response, overrides), url: url_for(response), description: description_for(response, overrides), language: language_for(response, overrides), ttl: ttl_for(response, overrides), last_build_date: last_build_date_for(response), image: image_for(response, overrides), author: (response, overrides) ) end private # Fallback RSS ttl (in minutes) when no cache directives are present. def default_ttl_in_minutes = 360 # Description template used when no explicit or discovered description exists. def default_description_template = 'Latest items from %<url>s' def url_for(response) = Html2rss::Url.from_absolute(response.url) def title_for(response, overrides) return overrides[:title] if overrides[:title] title = parsed_title(response) return title if title url_for(response).channel_titleized end def parsed_title(response) return unless html_response?(response) title = parsed_body(response).at_css('head > title')&.text.to_s return if title.empty? title.gsub(/\s+/, ' ').strip end def description_for(response, overrides) return overrides[:description] unless overrides[:description].to_s.empty? description = (response) return format(default_description_template, url: url_for(response)) if description.to_s.empty? description end def (response) return unless html_response?(response) parsed_body(response).at_css('meta[name="description"]')&.[]('content') end def ttl_for(response, overrides) calculated = if overrides[:ttl] overrides[:ttl].to_i elsif (max_age = headers(response)['cache-control']&.match(/max-age=(\d+)/)&.[](1)) max_age.to_i.fdiv(60).ceil else default_ttl_in_minutes end min_ttl = Html2rss.defaults.min_ttl min_ttl ? [calculated, min_ttl].max : calculated end def language_for(response, overrides) return overrides[:language] if overrides[:language] if (language_code = headers(response)['content-language']&.match(/^([a-z]{2})/)) return language_code[0] end return unless html_response?(response) parsed_body(response)['lang'] || parsed_body(response).at_css('[lang]')&.[]('lang') end def (response, overrides) return overrides[:author] if overrides[:author] return unless html_response?(response) parsed_body(response).at_css('meta[name="author"]')&.[]('content') end def last_build_date_for(response) header = headers(response)['last-modified'] return Time.now unless header Time.httpdate(header) rescue ArgumentError Time.now end def image_for(response, overrides) return overrides[:image] if overrides[:image] return unless html_response?(response) if (image_url = parsed_body(response).at_css('meta[property="og:image"]')&.[]('content')) Url.sanitize(image_url) end end def parsed_body(response) = response.parsed_body def headers(response) = response.headers def html_response?(response) = response.html_response? end ## # @param title [String] # @param url [Html2rss::Url, String] # @param description [String] # @param language [String, nil] # @param ttl [Integer] # @param last_build_date [Time, String] # @param image [Html2rss::Url, String, nil] # @param author [String, nil] # rubocop:disable Metrics/ParameterLists -- Data.define members are the channel contract def initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) super( title: freeze_string(title), url: url.is_a?(Url) ? url : Url.from_absolute(url), description: freeze_string(description), language: language && freeze_string(language), ttl: Integer(ttl), last_build_date: normalize_last_build_date(last_build_date), image: normalize_image(image), author: && freeze_string() ) end # rubocop:enable Metrics/ParameterLists private def marshal_dump = [title, url, description, language, ttl, last_build_date, image, ] def marshal_load((title, url, description, language, ttl, last_build_date, image, )) initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) end def freeze_string(value) = value.to_s.dup.freeze def normalize_image(value) return value if value.is_a?(Url) return if value.nil? Url.sanitize(value.to_s) end def normalize_last_build_date(value) return value if value.is_a?(Time) raise ArgumentError, 'last_build_date must be a Time or HTTP-date String' unless value.is_a?(String) Time.httpdate(value) end end |
#description ⇒ String (readonly)
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/html2rss/channel.rb', line 30 Channel = Data.define(:title, :url, :description, :language, :ttl, :last_build_date, :image, :author) do class << self ## # Materializes channel attributes from an HTTP response and optional overrides. # # @param response [Html2rss::RequestService::Response] # @param overrides [Hash{Symbol => Object}] optional overrides for channel attributes # @return [Html2rss::Channel] def from_response(response, overrides: {}) new( title: title_for(response, overrides), url: url_for(response), description: description_for(response, overrides), language: language_for(response, overrides), ttl: ttl_for(response, overrides), last_build_date: last_build_date_for(response), image: image_for(response, overrides), author: (response, overrides) ) end private # Fallback RSS ttl (in minutes) when no cache directives are present. def default_ttl_in_minutes = 360 # Description template used when no explicit or discovered description exists. def default_description_template = 'Latest items from %<url>s' def url_for(response) = Html2rss::Url.from_absolute(response.url) def title_for(response, overrides) return overrides[:title] if overrides[:title] title = parsed_title(response) return title if title url_for(response).channel_titleized end def parsed_title(response) return unless html_response?(response) title = parsed_body(response).at_css('head > title')&.text.to_s return if title.empty? title.gsub(/\s+/, ' ').strip end def description_for(response, overrides) return overrides[:description] unless overrides[:description].to_s.empty? description = (response) return format(default_description_template, url: url_for(response)) if description.to_s.empty? description end def (response) return unless html_response?(response) parsed_body(response).at_css('meta[name="description"]')&.[]('content') end def ttl_for(response, overrides) calculated = if overrides[:ttl] overrides[:ttl].to_i elsif (max_age = headers(response)['cache-control']&.match(/max-age=(\d+)/)&.[](1)) max_age.to_i.fdiv(60).ceil else default_ttl_in_minutes end min_ttl = Html2rss.defaults.min_ttl min_ttl ? [calculated, min_ttl].max : calculated end def language_for(response, overrides) return overrides[:language] if overrides[:language] if (language_code = headers(response)['content-language']&.match(/^([a-z]{2})/)) return language_code[0] end return unless html_response?(response) parsed_body(response)['lang'] || parsed_body(response).at_css('[lang]')&.[]('lang') end def (response, overrides) return overrides[:author] if overrides[:author] return unless html_response?(response) parsed_body(response).at_css('meta[name="author"]')&.[]('content') end def last_build_date_for(response) header = headers(response)['last-modified'] return Time.now unless header Time.httpdate(header) rescue ArgumentError Time.now end def image_for(response, overrides) return overrides[:image] if overrides[:image] return unless html_response?(response) if (image_url = parsed_body(response).at_css('meta[property="og:image"]')&.[]('content')) Url.sanitize(image_url) end end def parsed_body(response) = response.parsed_body def headers(response) = response.headers def html_response?(response) = response.html_response? end ## # @param title [String] # @param url [Html2rss::Url, String] # @param description [String] # @param language [String, nil] # @param ttl [Integer] # @param last_build_date [Time, String] # @param image [Html2rss::Url, String, nil] # @param author [String, nil] # rubocop:disable Metrics/ParameterLists -- Data.define members are the channel contract def initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) super( title: freeze_string(title), url: url.is_a?(Url) ? url : Url.from_absolute(url), description: freeze_string(description), language: language && freeze_string(language), ttl: Integer(ttl), last_build_date: normalize_last_build_date(last_build_date), image: normalize_image(image), author: && freeze_string() ) end # rubocop:enable Metrics/ParameterLists private def marshal_dump = [title, url, description, language, ttl, last_build_date, image, ] def marshal_load((title, url, description, language, ttl, last_build_date, image, )) initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) end def freeze_string(value) = value.to_s.dup.freeze def normalize_image(value) return value if value.is_a?(Url) return if value.nil? Url.sanitize(value.to_s) end def normalize_last_build_date(value) return value if value.is_a?(Time) raise ArgumentError, 'last_build_date must be a Time or HTTP-date String' unless value.is_a?(String) Time.httpdate(value) end end |
#image ⇒ Html2rss::Url? (readonly)
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/html2rss/channel.rb', line 30 Channel = Data.define(:title, :url, :description, :language, :ttl, :last_build_date, :image, :author) do class << self ## # Materializes channel attributes from an HTTP response and optional overrides. # # @param response [Html2rss::RequestService::Response] # @param overrides [Hash{Symbol => Object}] optional overrides for channel attributes # @return [Html2rss::Channel] def from_response(response, overrides: {}) new( title: title_for(response, overrides), url: url_for(response), description: description_for(response, overrides), language: language_for(response, overrides), ttl: ttl_for(response, overrides), last_build_date: last_build_date_for(response), image: image_for(response, overrides), author: (response, overrides) ) end private # Fallback RSS ttl (in minutes) when no cache directives are present. def default_ttl_in_minutes = 360 # Description template used when no explicit or discovered description exists. def default_description_template = 'Latest items from %<url>s' def url_for(response) = Html2rss::Url.from_absolute(response.url) def title_for(response, overrides) return overrides[:title] if overrides[:title] title = parsed_title(response) return title if title url_for(response).channel_titleized end def parsed_title(response) return unless html_response?(response) title = parsed_body(response).at_css('head > title')&.text.to_s return if title.empty? title.gsub(/\s+/, ' ').strip end def description_for(response, overrides) return overrides[:description] unless overrides[:description].to_s.empty? description = (response) return format(default_description_template, url: url_for(response)) if description.to_s.empty? description end def (response) return unless html_response?(response) parsed_body(response).at_css('meta[name="description"]')&.[]('content') end def ttl_for(response, overrides) calculated = if overrides[:ttl] overrides[:ttl].to_i elsif (max_age = headers(response)['cache-control']&.match(/max-age=(\d+)/)&.[](1)) max_age.to_i.fdiv(60).ceil else default_ttl_in_minutes end min_ttl = Html2rss.defaults.min_ttl min_ttl ? [calculated, min_ttl].max : calculated end def language_for(response, overrides) return overrides[:language] if overrides[:language] if (language_code = headers(response)['content-language']&.match(/^([a-z]{2})/)) return language_code[0] end return unless html_response?(response) parsed_body(response)['lang'] || parsed_body(response).at_css('[lang]')&.[]('lang') end def (response, overrides) return overrides[:author] if overrides[:author] return unless html_response?(response) parsed_body(response).at_css('meta[name="author"]')&.[]('content') end def last_build_date_for(response) header = headers(response)['last-modified'] return Time.now unless header Time.httpdate(header) rescue ArgumentError Time.now end def image_for(response, overrides) return overrides[:image] if overrides[:image] return unless html_response?(response) if (image_url = parsed_body(response).at_css('meta[property="og:image"]')&.[]('content')) Url.sanitize(image_url) end end def parsed_body(response) = response.parsed_body def headers(response) = response.headers def html_response?(response) = response.html_response? end ## # @param title [String] # @param url [Html2rss::Url, String] # @param description [String] # @param language [String, nil] # @param ttl [Integer] # @param last_build_date [Time, String] # @param image [Html2rss::Url, String, nil] # @param author [String, nil] # rubocop:disable Metrics/ParameterLists -- Data.define members are the channel contract def initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) super( title: freeze_string(title), url: url.is_a?(Url) ? url : Url.from_absolute(url), description: freeze_string(description), language: language && freeze_string(language), ttl: Integer(ttl), last_build_date: normalize_last_build_date(last_build_date), image: normalize_image(image), author: && freeze_string() ) end # rubocop:enable Metrics/ParameterLists private def marshal_dump = [title, url, description, language, ttl, last_build_date, image, ] def marshal_load((title, url, description, language, ttl, last_build_date, image, )) initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) end def freeze_string(value) = value.to_s.dup.freeze def normalize_image(value) return value if value.is_a?(Url) return if value.nil? Url.sanitize(value.to_s) end def normalize_last_build_date(value) return value if value.is_a?(Time) raise ArgumentError, 'last_build_date must be a Time or HTTP-date String' unless value.is_a?(String) Time.httpdate(value) end end |
#language ⇒ String? (readonly)
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/html2rss/channel.rb', line 30 Channel = Data.define(:title, :url, :description, :language, :ttl, :last_build_date, :image, :author) do class << self ## # Materializes channel attributes from an HTTP response and optional overrides. # # @param response [Html2rss::RequestService::Response] # @param overrides [Hash{Symbol => Object}] optional overrides for channel attributes # @return [Html2rss::Channel] def from_response(response, overrides: {}) new( title: title_for(response, overrides), url: url_for(response), description: description_for(response, overrides), language: language_for(response, overrides), ttl: ttl_for(response, overrides), last_build_date: last_build_date_for(response), image: image_for(response, overrides), author: (response, overrides) ) end private # Fallback RSS ttl (in minutes) when no cache directives are present. def default_ttl_in_minutes = 360 # Description template used when no explicit or discovered description exists. def default_description_template = 'Latest items from %<url>s' def url_for(response) = Html2rss::Url.from_absolute(response.url) def title_for(response, overrides) return overrides[:title] if overrides[:title] title = parsed_title(response) return title if title url_for(response).channel_titleized end def parsed_title(response) return unless html_response?(response) title = parsed_body(response).at_css('head > title')&.text.to_s return if title.empty? title.gsub(/\s+/, ' ').strip end def description_for(response, overrides) return overrides[:description] unless overrides[:description].to_s.empty? description = (response) return format(default_description_template, url: url_for(response)) if description.to_s.empty? description end def (response) return unless html_response?(response) parsed_body(response).at_css('meta[name="description"]')&.[]('content') end def ttl_for(response, overrides) calculated = if overrides[:ttl] overrides[:ttl].to_i elsif (max_age = headers(response)['cache-control']&.match(/max-age=(\d+)/)&.[](1)) max_age.to_i.fdiv(60).ceil else default_ttl_in_minutes end min_ttl = Html2rss.defaults.min_ttl min_ttl ? [calculated, min_ttl].max : calculated end def language_for(response, overrides) return overrides[:language] if overrides[:language] if (language_code = headers(response)['content-language']&.match(/^([a-z]{2})/)) return language_code[0] end return unless html_response?(response) parsed_body(response)['lang'] || parsed_body(response).at_css('[lang]')&.[]('lang') end def (response, overrides) return overrides[:author] if overrides[:author] return unless html_response?(response) parsed_body(response).at_css('meta[name="author"]')&.[]('content') end def last_build_date_for(response) header = headers(response)['last-modified'] return Time.now unless header Time.httpdate(header) rescue ArgumentError Time.now end def image_for(response, overrides) return overrides[:image] if overrides[:image] return unless html_response?(response) if (image_url = parsed_body(response).at_css('meta[property="og:image"]')&.[]('content')) Url.sanitize(image_url) end end def parsed_body(response) = response.parsed_body def headers(response) = response.headers def html_response?(response) = response.html_response? end ## # @param title [String] # @param url [Html2rss::Url, String] # @param description [String] # @param language [String, nil] # @param ttl [Integer] # @param last_build_date [Time, String] # @param image [Html2rss::Url, String, nil] # @param author [String, nil] # rubocop:disable Metrics/ParameterLists -- Data.define members are the channel contract def initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) super( title: freeze_string(title), url: url.is_a?(Url) ? url : Url.from_absolute(url), description: freeze_string(description), language: language && freeze_string(language), ttl: Integer(ttl), last_build_date: normalize_last_build_date(last_build_date), image: normalize_image(image), author: && freeze_string() ) end # rubocop:enable Metrics/ParameterLists private def marshal_dump = [title, url, description, language, ttl, last_build_date, image, ] def marshal_load((title, url, description, language, ttl, last_build_date, image, )) initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) end def freeze_string(value) = value.to_s.dup.freeze def normalize_image(value) return value if value.is_a?(Url) return if value.nil? Url.sanitize(value.to_s) end def normalize_last_build_date(value) return value if value.is_a?(Time) raise ArgumentError, 'last_build_date must be a Time or HTTP-date String' unless value.is_a?(String) Time.httpdate(value) end end |
#last_build_date ⇒ Time (readonly)
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/html2rss/channel.rb', line 30 Channel = Data.define(:title, :url, :description, :language, :ttl, :last_build_date, :image, :author) do class << self ## # Materializes channel attributes from an HTTP response and optional overrides. # # @param response [Html2rss::RequestService::Response] # @param overrides [Hash{Symbol => Object}] optional overrides for channel attributes # @return [Html2rss::Channel] def from_response(response, overrides: {}) new( title: title_for(response, overrides), url: url_for(response), description: description_for(response, overrides), language: language_for(response, overrides), ttl: ttl_for(response, overrides), last_build_date: last_build_date_for(response), image: image_for(response, overrides), author: (response, overrides) ) end private # Fallback RSS ttl (in minutes) when no cache directives are present. def default_ttl_in_minutes = 360 # Description template used when no explicit or discovered description exists. def default_description_template = 'Latest items from %<url>s' def url_for(response) = Html2rss::Url.from_absolute(response.url) def title_for(response, overrides) return overrides[:title] if overrides[:title] title = parsed_title(response) return title if title url_for(response).channel_titleized end def parsed_title(response) return unless html_response?(response) title = parsed_body(response).at_css('head > title')&.text.to_s return if title.empty? title.gsub(/\s+/, ' ').strip end def description_for(response, overrides) return overrides[:description] unless overrides[:description].to_s.empty? description = (response) return format(default_description_template, url: url_for(response)) if description.to_s.empty? description end def (response) return unless html_response?(response) parsed_body(response).at_css('meta[name="description"]')&.[]('content') end def ttl_for(response, overrides) calculated = if overrides[:ttl] overrides[:ttl].to_i elsif (max_age = headers(response)['cache-control']&.match(/max-age=(\d+)/)&.[](1)) max_age.to_i.fdiv(60).ceil else default_ttl_in_minutes end min_ttl = Html2rss.defaults.min_ttl min_ttl ? [calculated, min_ttl].max : calculated end def language_for(response, overrides) return overrides[:language] if overrides[:language] if (language_code = headers(response)['content-language']&.match(/^([a-z]{2})/)) return language_code[0] end return unless html_response?(response) parsed_body(response)['lang'] || parsed_body(response).at_css('[lang]')&.[]('lang') end def (response, overrides) return overrides[:author] if overrides[:author] return unless html_response?(response) parsed_body(response).at_css('meta[name="author"]')&.[]('content') end def last_build_date_for(response) header = headers(response)['last-modified'] return Time.now unless header Time.httpdate(header) rescue ArgumentError Time.now end def image_for(response, overrides) return overrides[:image] if overrides[:image] return unless html_response?(response) if (image_url = parsed_body(response).at_css('meta[property="og:image"]')&.[]('content')) Url.sanitize(image_url) end end def parsed_body(response) = response.parsed_body def headers(response) = response.headers def html_response?(response) = response.html_response? end ## # @param title [String] # @param url [Html2rss::Url, String] # @param description [String] # @param language [String, nil] # @param ttl [Integer] # @param last_build_date [Time, String] # @param image [Html2rss::Url, String, nil] # @param author [String, nil] # rubocop:disable Metrics/ParameterLists -- Data.define members are the channel contract def initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) super( title: freeze_string(title), url: url.is_a?(Url) ? url : Url.from_absolute(url), description: freeze_string(description), language: language && freeze_string(language), ttl: Integer(ttl), last_build_date: normalize_last_build_date(last_build_date), image: normalize_image(image), author: && freeze_string() ) end # rubocop:enable Metrics/ParameterLists private def marshal_dump = [title, url, description, language, ttl, last_build_date, image, ] def marshal_load((title, url, description, language, ttl, last_build_date, image, )) initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) end def freeze_string(value) = value.to_s.dup.freeze def normalize_image(value) return value if value.is_a?(Url) return if value.nil? Url.sanitize(value.to_s) end def normalize_last_build_date(value) return value if value.is_a?(Time) raise ArgumentError, 'last_build_date must be a Time or HTTP-date String' unless value.is_a?(String) Time.httpdate(value) end end |
#title ⇒ String (readonly)
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/html2rss/channel.rb', line 30 Channel = Data.define(:title, :url, :description, :language, :ttl, :last_build_date, :image, :author) do class << self ## # Materializes channel attributes from an HTTP response and optional overrides. # # @param response [Html2rss::RequestService::Response] # @param overrides [Hash{Symbol => Object}] optional overrides for channel attributes # @return [Html2rss::Channel] def from_response(response, overrides: {}) new( title: title_for(response, overrides), url: url_for(response), description: description_for(response, overrides), language: language_for(response, overrides), ttl: ttl_for(response, overrides), last_build_date: last_build_date_for(response), image: image_for(response, overrides), author: (response, overrides) ) end private # Fallback RSS ttl (in minutes) when no cache directives are present. def default_ttl_in_minutes = 360 # Description template used when no explicit or discovered description exists. def default_description_template = 'Latest items from %<url>s' def url_for(response) = Html2rss::Url.from_absolute(response.url) def title_for(response, overrides) return overrides[:title] if overrides[:title] title = parsed_title(response) return title if title url_for(response).channel_titleized end def parsed_title(response) return unless html_response?(response) title = parsed_body(response).at_css('head > title')&.text.to_s return if title.empty? title.gsub(/\s+/, ' ').strip end def description_for(response, overrides) return overrides[:description] unless overrides[:description].to_s.empty? description = (response) return format(default_description_template, url: url_for(response)) if description.to_s.empty? description end def (response) return unless html_response?(response) parsed_body(response).at_css('meta[name="description"]')&.[]('content') end def ttl_for(response, overrides) calculated = if overrides[:ttl] overrides[:ttl].to_i elsif (max_age = headers(response)['cache-control']&.match(/max-age=(\d+)/)&.[](1)) max_age.to_i.fdiv(60).ceil else default_ttl_in_minutes end min_ttl = Html2rss.defaults.min_ttl min_ttl ? [calculated, min_ttl].max : calculated end def language_for(response, overrides) return overrides[:language] if overrides[:language] if (language_code = headers(response)['content-language']&.match(/^([a-z]{2})/)) return language_code[0] end return unless html_response?(response) parsed_body(response)['lang'] || parsed_body(response).at_css('[lang]')&.[]('lang') end def (response, overrides) return overrides[:author] if overrides[:author] return unless html_response?(response) parsed_body(response).at_css('meta[name="author"]')&.[]('content') end def last_build_date_for(response) header = headers(response)['last-modified'] return Time.now unless header Time.httpdate(header) rescue ArgumentError Time.now end def image_for(response, overrides) return overrides[:image] if overrides[:image] return unless html_response?(response) if (image_url = parsed_body(response).at_css('meta[property="og:image"]')&.[]('content')) Url.sanitize(image_url) end end def parsed_body(response) = response.parsed_body def headers(response) = response.headers def html_response?(response) = response.html_response? end ## # @param title [String] # @param url [Html2rss::Url, String] # @param description [String] # @param language [String, nil] # @param ttl [Integer] # @param last_build_date [Time, String] # @param image [Html2rss::Url, String, nil] # @param author [String, nil] # rubocop:disable Metrics/ParameterLists -- Data.define members are the channel contract def initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) super( title: freeze_string(title), url: url.is_a?(Url) ? url : Url.from_absolute(url), description: freeze_string(description), language: language && freeze_string(language), ttl: Integer(ttl), last_build_date: normalize_last_build_date(last_build_date), image: normalize_image(image), author: && freeze_string() ) end # rubocop:enable Metrics/ParameterLists private def marshal_dump = [title, url, description, language, ttl, last_build_date, image, ] def marshal_load((title, url, description, language, ttl, last_build_date, image, )) initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) end def freeze_string(value) = value.to_s.dup.freeze def normalize_image(value) return value if value.is_a?(Url) return if value.nil? Url.sanitize(value.to_s) end def normalize_last_build_date(value) return value if value.is_a?(Time) raise ArgumentError, 'last_build_date must be a Time or HTTP-date String' unless value.is_a?(String) Time.httpdate(value) end end |
#ttl ⇒ Integer (readonly)
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/html2rss/channel.rb', line 30 Channel = Data.define(:title, :url, :description, :language, :ttl, :last_build_date, :image, :author) do class << self ## # Materializes channel attributes from an HTTP response and optional overrides. # # @param response [Html2rss::RequestService::Response] # @param overrides [Hash{Symbol => Object}] optional overrides for channel attributes # @return [Html2rss::Channel] def from_response(response, overrides: {}) new( title: title_for(response, overrides), url: url_for(response), description: description_for(response, overrides), language: language_for(response, overrides), ttl: ttl_for(response, overrides), last_build_date: last_build_date_for(response), image: image_for(response, overrides), author: (response, overrides) ) end private # Fallback RSS ttl (in minutes) when no cache directives are present. def default_ttl_in_minutes = 360 # Description template used when no explicit or discovered description exists. def default_description_template = 'Latest items from %<url>s' def url_for(response) = Html2rss::Url.from_absolute(response.url) def title_for(response, overrides) return overrides[:title] if overrides[:title] title = parsed_title(response) return title if title url_for(response).channel_titleized end def parsed_title(response) return unless html_response?(response) title = parsed_body(response).at_css('head > title')&.text.to_s return if title.empty? title.gsub(/\s+/, ' ').strip end def description_for(response, overrides) return overrides[:description] unless overrides[:description].to_s.empty? description = (response) return format(default_description_template, url: url_for(response)) if description.to_s.empty? description end def (response) return unless html_response?(response) parsed_body(response).at_css('meta[name="description"]')&.[]('content') end def ttl_for(response, overrides) calculated = if overrides[:ttl] overrides[:ttl].to_i elsif (max_age = headers(response)['cache-control']&.match(/max-age=(\d+)/)&.[](1)) max_age.to_i.fdiv(60).ceil else default_ttl_in_minutes end min_ttl = Html2rss.defaults.min_ttl min_ttl ? [calculated, min_ttl].max : calculated end def language_for(response, overrides) return overrides[:language] if overrides[:language] if (language_code = headers(response)['content-language']&.match(/^([a-z]{2})/)) return language_code[0] end return unless html_response?(response) parsed_body(response)['lang'] || parsed_body(response).at_css('[lang]')&.[]('lang') end def (response, overrides) return overrides[:author] if overrides[:author] return unless html_response?(response) parsed_body(response).at_css('meta[name="author"]')&.[]('content') end def last_build_date_for(response) header = headers(response)['last-modified'] return Time.now unless header Time.httpdate(header) rescue ArgumentError Time.now end def image_for(response, overrides) return overrides[:image] if overrides[:image] return unless html_response?(response) if (image_url = parsed_body(response).at_css('meta[property="og:image"]')&.[]('content')) Url.sanitize(image_url) end end def parsed_body(response) = response.parsed_body def headers(response) = response.headers def html_response?(response) = response.html_response? end ## # @param title [String] # @param url [Html2rss::Url, String] # @param description [String] # @param language [String, nil] # @param ttl [Integer] # @param last_build_date [Time, String] # @param image [Html2rss::Url, String, nil] # @param author [String, nil] # rubocop:disable Metrics/ParameterLists -- Data.define members are the channel contract def initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) super( title: freeze_string(title), url: url.is_a?(Url) ? url : Url.from_absolute(url), description: freeze_string(description), language: language && freeze_string(language), ttl: Integer(ttl), last_build_date: normalize_last_build_date(last_build_date), image: normalize_image(image), author: && freeze_string() ) end # rubocop:enable Metrics/ParameterLists private def marshal_dump = [title, url, description, language, ttl, last_build_date, image, ] def marshal_load((title, url, description, language, ttl, last_build_date, image, )) initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) end def freeze_string(value) = value.to_s.dup.freeze def normalize_image(value) return value if value.is_a?(Url) return if value.nil? Url.sanitize(value.to_s) end def normalize_last_build_date(value) return value if value.is_a?(Time) raise ArgumentError, 'last_build_date must be a Time or HTTP-date String' unless value.is_a?(String) Time.httpdate(value) end end |
#url ⇒ Html2rss::Url (readonly)
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/html2rss/channel.rb', line 30 Channel = Data.define(:title, :url, :description, :language, :ttl, :last_build_date, :image, :author) do class << self ## # Materializes channel attributes from an HTTP response and optional overrides. # # @param response [Html2rss::RequestService::Response] # @param overrides [Hash{Symbol => Object}] optional overrides for channel attributes # @return [Html2rss::Channel] def from_response(response, overrides: {}) new( title: title_for(response, overrides), url: url_for(response), description: description_for(response, overrides), language: language_for(response, overrides), ttl: ttl_for(response, overrides), last_build_date: last_build_date_for(response), image: image_for(response, overrides), author: (response, overrides) ) end private # Fallback RSS ttl (in minutes) when no cache directives are present. def default_ttl_in_minutes = 360 # Description template used when no explicit or discovered description exists. def default_description_template = 'Latest items from %<url>s' def url_for(response) = Html2rss::Url.from_absolute(response.url) def title_for(response, overrides) return overrides[:title] if overrides[:title] title = parsed_title(response) return title if title url_for(response).channel_titleized end def parsed_title(response) return unless html_response?(response) title = parsed_body(response).at_css('head > title')&.text.to_s return if title.empty? title.gsub(/\s+/, ' ').strip end def description_for(response, overrides) return overrides[:description] unless overrides[:description].to_s.empty? description = (response) return format(default_description_template, url: url_for(response)) if description.to_s.empty? description end def (response) return unless html_response?(response) parsed_body(response).at_css('meta[name="description"]')&.[]('content') end def ttl_for(response, overrides) calculated = if overrides[:ttl] overrides[:ttl].to_i elsif (max_age = headers(response)['cache-control']&.match(/max-age=(\d+)/)&.[](1)) max_age.to_i.fdiv(60).ceil else default_ttl_in_minutes end min_ttl = Html2rss.defaults.min_ttl min_ttl ? [calculated, min_ttl].max : calculated end def language_for(response, overrides) return overrides[:language] if overrides[:language] if (language_code = headers(response)['content-language']&.match(/^([a-z]{2})/)) return language_code[0] end return unless html_response?(response) parsed_body(response)['lang'] || parsed_body(response).at_css('[lang]')&.[]('lang') end def (response, overrides) return overrides[:author] if overrides[:author] return unless html_response?(response) parsed_body(response).at_css('meta[name="author"]')&.[]('content') end def last_build_date_for(response) header = headers(response)['last-modified'] return Time.now unless header Time.httpdate(header) rescue ArgumentError Time.now end def image_for(response, overrides) return overrides[:image] if overrides[:image] return unless html_response?(response) if (image_url = parsed_body(response).at_css('meta[property="og:image"]')&.[]('content')) Url.sanitize(image_url) end end def parsed_body(response) = response.parsed_body def headers(response) = response.headers def html_response?(response) = response.html_response? end ## # @param title [String] # @param url [Html2rss::Url, String] # @param description [String] # @param language [String, nil] # @param ttl [Integer] # @param last_build_date [Time, String] # @param image [Html2rss::Url, String, nil] # @param author [String, nil] # rubocop:disable Metrics/ParameterLists -- Data.define members are the channel contract def initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) super( title: freeze_string(title), url: url.is_a?(Url) ? url : Url.from_absolute(url), description: freeze_string(description), language: language && freeze_string(language), ttl: Integer(ttl), last_build_date: normalize_last_build_date(last_build_date), image: normalize_image(image), author: && freeze_string() ) end # rubocop:enable Metrics/ParameterLists private def marshal_dump = [title, url, description, language, ttl, last_build_date, image, ] def marshal_load((title, url, description, language, ttl, last_build_date, image, )) initialize(title:, url:, description:, language:, ttl:, last_build_date:, image:, author:) end def freeze_string(value) = value.to_s.dup.freeze def normalize_image(value) return value if value.is_a?(Url) return if value.nil? Url.sanitize(value.to_s) end def normalize_last_build_date(value) return value if value.is_a?(Time) raise ArgumentError, 'last_build_date must be a Time or HTTP-date String' unless value.is_a?(String) Time.httpdate(value) end end |
Class Method Details
.from_response(response, overrides: {}) ⇒ Html2rss::Channel
Materializes channel attributes from an HTTP response and optional overrides.
38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/html2rss/channel.rb', line 38 def from_response(response, overrides: {}) new( title: title_for(response, overrides), url: url_for(response), description: description_for(response, overrides), language: language_for(response, overrides), ttl: ttl_for(response, overrides), last_build_date: last_build_date_for(response), image: image_for(response, overrides), author: (response, overrides) ) end |