110 lines
3.5 KiB
Ruby
Raw Normal View History

2020-12-18 17:58:21 +01:00
# typed: true
2020-12-12 21:59:04 +01:00
# frozen_string_literal: true
require "bundle_version"
2020-12-12 21:59:04 +01:00
require_relative "page_match"
module Homebrew
module Livecheck
module Strategy
# The {Sparkle} strategy fetches content at a URL and parses
2020-12-20 00:23:22 -05:00
# it as a Sparkle appcast in XML format.
2020-12-12 21:59:04 +01:00
#
# @api private
class Sparkle
2020-12-12 21:59:04 +01:00
extend T::Sig
# A priority of zero causes livecheck to skip the strategy. We only
# apply {Sparkle} using `strategy :sparkle` in a `livecheck` block,
# as we can't automatically determine when this can be successfully
# applied to a URL without fetching the content.
PRIORITY = 0
# The `Regexp` used to determine if the strategy applies to the URL.
URL_MATCH_REGEX = %r{^https?://}i.freeze
# Whether the strategy can be applied to the provided URL.
# The strategy will technically match any HTTP URL but is
# only usable with a `livecheck` block containing a regex
# or block.
sig { params(url: String).returns(T::Boolean) }
def self.match?(url)
URL_MATCH_REGEX.match?(url)
end
2020-12-19 01:07:56 -05:00
Item = Struct.new(:title, :url, :bundle_version, :short_version, :version, keyword_init: true) do
extend T::Sig
2020-12-15 20:26:19 +01:00
2020-12-19 01:07:56 -05:00
extend Forwardable
2020-12-15 20:26:19 +01:00
2020-12-19 01:07:56 -05:00
delegate version: :bundle_version
delegate short_version: :bundle_version
2020-12-15 20:26:19 +01:00
end
2020-12-17 15:40:23 +01:00
sig { params(content: String).returns(T.nilable(Item)) }
2020-12-15 20:26:19 +01:00
def self.item_from_content(content)
require "nokogiri"
xml = Nokogiri::XML(content)
2020-12-12 21:59:04 +01:00
xml.remove_namespaces!
items = xml.xpath("//rss//channel//item").map do |item|
enclosure = (item > "enclosure").first
2020-12-12 21:59:04 +01:00
url = enclosure&.attr("url")
short_version = enclosure&.attr("shortVersionString")
version = enclosure&.attr("version")
url ||= (item > "link").first&.text
short_version ||= (item > "shortVersionString").first&.text&.strip
version ||= (item > "version").first&.text&.strip
title = (item > "title").first&.text&.strip
if match = title&.match(/(\d+(?:\.\d+)*)\s*(\([^)]+\))?\Z/)
short_version ||= match[1]
version ||= match[2]
end
2020-12-18 21:17:55 +01:00
bundle_version = BundleVersion.new(short_version, version) if short_version || version
data = {
title: title,
url: url,
2020-12-18 21:17:55 +01:00
bundle_version: bundle_version,
short_version: bundle_version&.short_version,
version: bundle_version&.version,
}.compact
2020-12-15 20:26:19 +01:00
Item.new(**data) unless data.empty?
end.compact
2020-12-17 15:40:23 +01:00
items.max_by(&:bundle_version)
2020-12-15 20:26:19 +01:00
end
2020-12-19 01:07:56 -05:00
# Checks the content at the URL for new versions.
sig { params(url: String, regex: T.nilable(Regexp)).returns(T::Hash[Symbol, T.untyped]) }
def self.find_versions(url, regex, &block)
2020-12-19 02:22:03 -05:00
raise ArgumentError, "The #{T.must(name).demodulize} strategy does not support a regex." if regex
2020-12-19 01:07:56 -05:00
match_data = { matches: {}, regex: regex, url: url }
2020-12-12 21:59:04 +01:00
2020-12-20 00:23:22 -05:00
content = Strategy.page_content(url)
2020-12-19 01:07:56 -05:00
2020-12-20 00:23:22 -05:00
if (item = item_from_content(content))
2020-12-19 01:07:56 -05:00
match = if block
block.call(item)&.to_s
else
item.bundle_version&.nice_version
end
match_data[:matches][match] = Version.new(match) if match
end
match_data
2020-12-12 21:59:04 +01:00
end
end
end
end
end