Sam Ford 56dd89114d
Standardize valid strategy block return types
Valid `strategy` block return types currently vary between
strategies. Some only accept a string whereas others accept a string
or array of strings. [`strategy` blocks also accept a `nil` return
(to simplify early returns) but this was already standardized across
strategies.]

While some strategies only identify one version by default (where a
string is an appropriate return type), it could be that a strategy
block identifies more than one version. In this situation, the
strategy would need to be modified to accept (and work with) an
array from a `strategy` block.

Rather than waiting for this to become a problem, this modifies all
strategies to standardize on allowing `strategy` blocks to return a
string or array of strings (even if only one of these is currently
used in practice). Standardizing valid return types helps to further
simplify the mental model for `strategy` blocks and reduce cognitive
load.

This commit extracts related logic from `#find_versions` into
methods like `#versions_from_content`, which is conceptually similar
to `PageMatch#page_matches` (renamed to `#versions_from_content`
for consistency). This allows us to write tests for the related code
without having to make network requests (or stub them) at this point.
In general, this also helps to better align the structure of
strategies and how the various `#find_versions` methods work with
versions.

There's still more planned work to be done here but this is a step
in the right direction.
2021-08-11 19:05:57 -04:00

111 lines
3.9 KiB
Ruby

# typed: true
# frozen_string_literal: true
module Homebrew
module Livecheck
module Strategy
# The {PageMatch} strategy fetches content at a URL and scans it for
# matching text using the provided regex.
#
# This strategy can be used in a `livecheck` block when no specific
# strategies apply to a given URL. Though {PageMatch} will technically
# match any HTTP URL, the strategy also requires a regex to function.
#
# The {find_versions} method is also used within other
# strategies, to handle the process of identifying version text in
# content.
#
# @api public
class PageMatch
extend T::Sig
NICE_NAME = "Page match"
# A priority of zero causes livecheck to skip the strategy. We do this
# for PageMatch so we can selectively apply the strategy only when a
# regex is provided in a `livecheck` block.
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.
# PageMatch will technically match any HTTP URL but is only
# usable with a `livecheck` block containing a regex.
sig { params(url: String).returns(T::Boolean) }
def self.match?(url)
URL_MATCH_REGEX.match?(url)
end
# Uses the regex to match text in the content or, if a block is
# provided, passes the page content to the block to handle matching.
# With either approach, an array of unique matches is returned.
#
# @param content [String] the page content to check
# @param regex [Regexp] a regex used for matching versions in the
# content
# @return [Array]
sig {
params(
content: String,
regex: Regexp,
block: T.nilable(
T.proc.params(arg0: String, arg1: Regexp).returns(T.any(String, T::Array[String], NilClass)),
),
).returns(T::Array[String])
}
def self.versions_from_content(content, regex, &block)
return Strategy.handle_block_return(block.call(content, regex)) if block
content.scan(regex).map do |match|
case match
when String
match
when Array
match.first
end
end.compact.uniq
end
# Checks the content at the URL for new versions, using the provided
# regex for matching.
#
# @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content
# @param provided_content [String] page content to use in place of
# fetching via Strategy#page_content
# @return [Hash]
sig {
params(
url: String,
regex: Regexp,
cask: T.nilable(Cask::Cask),
provided_content: T.nilable(String),
block: T.nilable(
T.proc.params(arg0: String, arg1: Regexp).returns(T.any(String, T::Array[String], NilClass)),
),
).returns(T::Hash[Symbol, T.untyped])
}
def self.find_versions(url, regex, cask: nil, provided_content: nil, &block)
match_data = { matches: {}, regex: regex, url: url }
content = if provided_content.is_a?(String)
match_data[:cached] = true
provided_content
else
match_data.merge!(Strategy.page_content(url))
match_data[:content]
end
return match_data if content.blank?
versions_from_content(content, regex, &block).each do |match_text|
match_data[:matches][match_text] = Version.new(match_text)
end
match_data
end
end
end
end
end