111 lines
		
	
	
		
			3.5 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			111 lines
		
	
	
		
			3.5 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| # typed: true
 | |
| # frozen_string_literal: true
 | |
| 
 | |
| require "bundle_version"
 | |
| require_relative "page_match"
 | |
| 
 | |
| module Homebrew
 | |
|   module Livecheck
 | |
|     module Strategy
 | |
|       # The {Sparkle} strategy fetches content at a URL and parses
 | |
|       # it as a Sparkle appcast in XML format.
 | |
|       #
 | |
|       # @api private
 | |
|       class Sparkle
 | |
|         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
 | |
| 
 | |
|         Item = Struct.new(:title, :url, :bundle_version, :short_version, :version, keyword_init: true) do
 | |
|           extend T::Sig
 | |
| 
 | |
|           extend Forwardable
 | |
| 
 | |
|           delegate version: :bundle_version
 | |
|           delegate short_version: :bundle_version
 | |
|         end
 | |
| 
 | |
|         sig { params(content: String).returns(T.nilable(Item)) }
 | |
|         def self.item_from_content(content)
 | |
|           require "nokogiri"
 | |
| 
 | |
|           xml = Nokogiri::XML(content)
 | |
|           xml.remove_namespaces!
 | |
| 
 | |
|           items = xml.xpath("//rss//channel//item").map do |item|
 | |
|             enclosure = (item > "enclosure").first
 | |
| 
 | |
|             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
 | |
| 
 | |
|             bundle_version = BundleVersion.new(short_version, version) if short_version || version
 | |
| 
 | |
|             data = {
 | |
|               title:          title,
 | |
|               url:            url,
 | |
|               bundle_version: bundle_version,
 | |
|               short_version:  bundle_version&.short_version,
 | |
|               version:        bundle_version&.version,
 | |
|             }.compact
 | |
| 
 | |
|             Item.new(**data) unless data.empty?
 | |
|           end.compact
 | |
| 
 | |
|           items.max_by(&:bundle_version)
 | |
|         end
 | |
| 
 | |
|         # 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)
 | |
|           raise ArgumentError, "The #{T.must(name).demodulize} strategy does not support a regex." if regex
 | |
| 
 | |
|           match_data = { matches: {}, regex: regex, url: url }
 | |
| 
 | |
|           match_data.merge!(Strategy.page_content(url))
 | |
|           content = match_data.delete(:content)
 | |
| 
 | |
|           if (item = item_from_content(content))
 | |
|             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
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| end
 | 
