livecheck: Add support for POST requests
livecheck currently doesn't support `POST` requests but it wasn't entirely clear how best to handle that. I initially approached it as a `Post` strategy but unfortunately that would have required us to handle response body parsing (e.g., JSON, XML, etc.) in some fashion. We could borrow some of the logic from related strategies but we would still be stuck having to update `Post` whenever we add a strategy for a new format. Instead, this implements `POST` support by borrowing ideas from the `using: :post` and `data` `url` options found in formulae. This uses a `post_form` option to handle form data and `post_json` to handle JSON data, encoding the hash argument for each into the appropriate format. The presence of either option means that curl will use a `POST` request. With this approach, we can make a `POST` request using any strategy that calls `Strategy::page_headers` or `::page_content` (directly or indirectly) and everything else works the same as usual. The only change needed in related strategies was to pass the options through to the `Strategy` methods. For example, if we need to parse a JSON response from a `POST` request, we add a `post_data` or `post_json` hash to the `livecheck` block `url` and use `strategy :json` with a `strategy` block. This leans on existing patterns that we're already familiar with and shouldn't require any notable maintenance burden when adding new strategies, so it seems like a better approach than a `Post` strategy.
This commit is contained in:
parent
94e2bdf668
commit
b4757af656
@ -20,6 +20,14 @@ class Livecheck
|
|||||||
sig { returns(T.nilable(String)) }
|
sig { returns(T.nilable(String)) }
|
||||||
attr_reader :skip_msg
|
attr_reader :skip_msg
|
||||||
|
|
||||||
|
# A block used by strategies to identify version information.
|
||||||
|
sig { returns(T.nilable(Proc)) }
|
||||||
|
attr_reader :strategy_block
|
||||||
|
|
||||||
|
# Options used by `Strategy` methods to modify `curl` behavior.
|
||||||
|
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
||||||
|
attr_reader :url_options
|
||||||
|
|
||||||
sig { params(package_or_resource: T.any(Cask::Cask, T.class_of(Formula), Resource)).void }
|
sig { params(package_or_resource: T.any(Cask::Cask, T.class_of(Formula), Resource)).void }
|
||||||
def initialize(package_or_resource)
|
def initialize(package_or_resource)
|
||||||
@package_or_resource = package_or_resource
|
@package_or_resource = package_or_resource
|
||||||
@ -32,6 +40,7 @@ class Livecheck
|
|||||||
@strategy_block = T.let(nil, T.nilable(Proc))
|
@strategy_block = T.let(nil, T.nilable(Proc))
|
||||||
@throttle = T.let(nil, T.nilable(Integer))
|
@throttle = T.let(nil, T.nilable(Integer))
|
||||||
@url = T.let(nil, T.any(NilClass, String, Symbol))
|
@url = T.let(nil, T.any(NilClass, String, Symbol))
|
||||||
|
@url_options = T.let(nil, T.nilable(T::Hash[Symbol, T.untyped]))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sets the `@referenced_cask_name` instance variable to the provided `String`
|
# Sets the `@referenced_cask_name` instance variable to the provided `String`
|
||||||
@ -134,9 +143,6 @@ class Livecheck
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
sig { returns(T.nilable(Proc)) }
|
|
||||||
attr_reader :strategy_block
|
|
||||||
|
|
||||||
# Sets the `@throttle` instance variable to the provided `Integer` or returns
|
# Sets the `@throttle` instance variable to the provided `Integer` or returns
|
||||||
# the `@throttle` instance variable when no argument is provided.
|
# the `@throttle` instance variable when no argument is provided.
|
||||||
sig {
|
sig {
|
||||||
@ -158,13 +164,22 @@ class Livecheck
|
|||||||
# `@url` instance variable when no argument is provided. The argument can be
|
# `@url` instance variable when no argument is provided. The argument can be
|
||||||
# a `String` (a URL) or a supported `Symbol` corresponding to a URL in the
|
# a `String` (a URL) or a supported `Symbol` corresponding to a URL in the
|
||||||
# formula/cask/resource (e.g. `:stable`, `:homepage`, `:head`, `:url`).
|
# formula/cask/resource (e.g. `:stable`, `:homepage`, `:head`, `:url`).
|
||||||
|
# Any options provided to the method are passed through to `Strategy` methods
|
||||||
|
# (`page_headers`, `page_content`).
|
||||||
sig {
|
sig {
|
||||||
params(
|
params(
|
||||||
# URL to check for version information.
|
# URL to check for version information.
|
||||||
url: T.any(String, Symbol),
|
url: T.any(String, Symbol),
|
||||||
|
post_form: T.nilable(T::Hash[T.any(String, Symbol), String]),
|
||||||
|
post_json: T.nilable(T::Hash[T.any(String, Symbol), String]),
|
||||||
).returns(T.nilable(T.any(String, Symbol)))
|
).returns(T.nilable(T.any(String, Symbol)))
|
||||||
}
|
}
|
||||||
def url(url = T.unsafe(nil))
|
def url(url = T.unsafe(nil), post_form: nil, post_json: nil)
|
||||||
|
raise ArgumentError, "Only use `post_form` or `post_json`, not both" if post_form && post_json
|
||||||
|
|
||||||
|
options = { post_form:, post_json: }.compact
|
||||||
|
@url_options = options if options.present?
|
||||||
|
|
||||||
case url
|
case url
|
||||||
when nil
|
when nil
|
||||||
@url
|
@url
|
||||||
@ -183,14 +198,15 @@ class Livecheck
|
|||||||
sig { returns(T::Hash[String, T.untyped]) }
|
sig { returns(T::Hash[String, T.untyped]) }
|
||||||
def to_hash
|
def to_hash
|
||||||
{
|
{
|
||||||
"cask" => @referenced_cask_name,
|
"cask" => @referenced_cask_name,
|
||||||
"formula" => @referenced_formula_name,
|
"formula" => @referenced_formula_name,
|
||||||
"regex" => @regex,
|
"regex" => @regex,
|
||||||
"skip" => @skip,
|
"skip" => @skip,
|
||||||
"skip_msg" => @skip_msg,
|
"skip_msg" => @skip_msg,
|
||||||
"strategy" => @strategy,
|
"strategy" => @strategy,
|
||||||
"throttle" => @throttle,
|
"throttle" => @throttle,
|
||||||
"url" => @url,
|
"url" => @url,
|
||||||
|
"url_options" => @url_options,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -614,6 +614,7 @@ module Homebrew
|
|||||||
referenced_livecheck = referenced_formula_or_cask&.livecheck
|
referenced_livecheck = referenced_formula_or_cask&.livecheck
|
||||||
|
|
||||||
livecheck_url = livecheck.url || referenced_livecheck&.url
|
livecheck_url = livecheck.url || referenced_livecheck&.url
|
||||||
|
livecheck_url_options = livecheck.url_options || referenced_livecheck&.url_options
|
||||||
livecheck_regex = livecheck.regex || referenced_livecheck&.regex
|
livecheck_regex = livecheck.regex || referenced_livecheck&.regex
|
||||||
livecheck_strategy = livecheck.strategy || referenced_livecheck&.strategy
|
livecheck_strategy = livecheck.strategy || referenced_livecheck&.strategy
|
||||||
livecheck_strategy_block = livecheck.strategy_block || referenced_livecheck&.strategy_block
|
livecheck_strategy_block = livecheck.strategy_block || referenced_livecheck&.strategy_block
|
||||||
@ -673,6 +674,7 @@ module Homebrew
|
|||||||
elsif original_url.present? && original_url != "None"
|
elsif original_url.present? && original_url != "None"
|
||||||
puts "URL: #{original_url}"
|
puts "URL: #{original_url}"
|
||||||
end
|
end
|
||||||
|
puts "URL Options: #{livecheck_url_options}" if livecheck_url_options.present?
|
||||||
puts "URL (processed): #{url}" if url != original_url
|
puts "URL (processed): #{url}" if url != original_url
|
||||||
if strategies.present? && verbose
|
if strategies.present? && verbose
|
||||||
puts "Strategies: #{strategies.map { |s| livecheck_strategy_names[s] }.join(", ")}"
|
puts "Strategies: #{strategies.map { |s| livecheck_strategy_names[s] }.join(", ")}"
|
||||||
@ -701,6 +703,7 @@ module Homebrew
|
|||||||
|
|
||||||
strategy_args = {
|
strategy_args = {
|
||||||
regex: livecheck_regex,
|
regex: livecheck_regex,
|
||||||
|
url_options: livecheck_url_options,
|
||||||
homebrew_curl:,
|
homebrew_curl:,
|
||||||
}
|
}
|
||||||
# TODO: Set `cask`/`url` args based on the presence of the keyword arg
|
# TODO: Set `cask`/`url` args based on the presence of the keyword arg
|
||||||
@ -807,6 +810,7 @@ module Homebrew
|
|||||||
version_info[:meta][:url][:strategy] = strategy_data[:url]
|
version_info[:meta][:url][:strategy] = strategy_data[:url]
|
||||||
end
|
end
|
||||||
version_info[:meta][:url][:final] = strategy_data[:final_url] if strategy_data[:final_url]
|
version_info[:meta][:url][:final] = strategy_data[:final_url] if strategy_data[:final_url]
|
||||||
|
version_info[:meta][:url][:options] = livecheck_url_options if livecheck_url_options.present?
|
||||||
version_info[:meta][:url][:homebrew_curl] = homebrew_curl if homebrew_curl.present?
|
version_info[:meta][:url][:homebrew_curl] = homebrew_curl if homebrew_curl.present?
|
||||||
end
|
end
|
||||||
version_info[:meta][:strategy] = strategy_name if strategy.present?
|
version_info[:meta][:strategy] = strategy_name if strategy.present?
|
||||||
@ -856,6 +860,7 @@ module Homebrew
|
|||||||
livecheck = resource.livecheck
|
livecheck = resource.livecheck
|
||||||
livecheck_reference = livecheck.formula
|
livecheck_reference = livecheck.formula
|
||||||
livecheck_url = livecheck.url
|
livecheck_url = livecheck.url
|
||||||
|
livecheck_url_options = livecheck.url_options
|
||||||
livecheck_regex = livecheck.regex
|
livecheck_regex = livecheck.regex
|
||||||
livecheck_strategy = livecheck.strategy
|
livecheck_strategy = livecheck.strategy
|
||||||
livecheck_strategy_block = livecheck.strategy_block
|
livecheck_strategy_block = livecheck.strategy_block
|
||||||
@ -893,6 +898,7 @@ module Homebrew
|
|||||||
elsif original_url.present? && original_url != "None"
|
elsif original_url.present? && original_url != "None"
|
||||||
puts "URL: #{original_url}"
|
puts "URL: #{original_url}"
|
||||||
end
|
end
|
||||||
|
puts "URL Options: #{livecheck_url_options}" if livecheck_url_options.present?
|
||||||
puts "URL (processed): #{url}" if url != original_url
|
puts "URL (processed): #{url}" if url != original_url
|
||||||
if strategies.present? && verbose
|
if strategies.present? && verbose
|
||||||
puts "Strategies: #{strategies.map { |s| livecheck_strategy_names[s] }.join(", ")}"
|
puts "Strategies: #{strategies.map { |s| livecheck_strategy_names[s] }.join(", ")}"
|
||||||
@ -923,6 +929,7 @@ module Homebrew
|
|||||||
strategy_args = {
|
strategy_args = {
|
||||||
url:,
|
url:,
|
||||||
regex: livecheck_regex,
|
regex: livecheck_regex,
|
||||||
|
url_options: livecheck_url_options,
|
||||||
homebrew_curl: false,
|
homebrew_curl: false,
|
||||||
}.compact
|
}.compact
|
||||||
|
|
||||||
@ -1012,6 +1019,7 @@ module Homebrew
|
|||||||
resource_version_info[:meta][:url][:strategy] = strategy_data[:url]
|
resource_version_info[:meta][:url][:strategy] = strategy_data[:url]
|
||||||
end
|
end
|
||||||
resource_version_info[:meta][:url][:final] = strategy_data[:final_url] if strategy_data&.dig(:final_url)
|
resource_version_info[:meta][:url][:final] = strategy_data[:final_url] if strategy_data&.dig(:final_url)
|
||||||
|
resource_version_info[:meta][:url][:options] = livecheck_url_options if livecheck_url_options.present?
|
||||||
end
|
end
|
||||||
resource_version_info[:meta][:strategy] = strategy_name if strategy.present?
|
resource_version_info[:meta][:strategy] = strategy_name if strategy.present?
|
||||||
if strategies.present?
|
if strategies.present?
|
||||||
|
@ -166,20 +166,59 @@ module Homebrew
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Creates `curl` `--data` or `--json` arguments (for `POST` requests`)
|
||||||
|
# from related `livecheck` block `url` options.
|
||||||
|
#
|
||||||
|
# @param post_form [Hash, nil] data to encode using `URI::encode_www_form`
|
||||||
|
# @param post_json [Hash, nil] data to encode using `JSON::generate`
|
||||||
|
# @return [Array]
|
||||||
|
sig {
|
||||||
|
params(
|
||||||
|
post_form: T.nilable(T::Hash[T.any(String, Symbol), String]),
|
||||||
|
post_json: T.nilable(T::Hash[T.any(String, Symbol), String]),
|
||||||
|
).returns(T::Array[String])
|
||||||
|
}
|
||||||
|
def post_args(post_form: nil, post_json: nil)
|
||||||
|
if post_form.present?
|
||||||
|
require "uri"
|
||||||
|
["--data", URI.encode_www_form(post_form)]
|
||||||
|
elsif post_json.present?
|
||||||
|
require "json"
|
||||||
|
["--json", JSON.generate(post_json)]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Collects HTTP response headers, starting with the provided URL.
|
# Collects HTTP response headers, starting with the provided URL.
|
||||||
# Redirections will be followed and all the response headers are
|
# Redirections will be followed and all the response headers are
|
||||||
# collected into an array of hashes.
|
# collected into an array of hashes.
|
||||||
#
|
#
|
||||||
# @param url [String] the URL to fetch
|
# @param url [String] the URL to fetch
|
||||||
|
# @param url_options [Hash] options to modify curl behavior
|
||||||
# @param homebrew_curl [Boolean] whether to use brewed curl with the URL
|
# @param homebrew_curl [Boolean] whether to use brewed curl with the URL
|
||||||
# @return [Array]
|
# @return [Array]
|
||||||
sig { params(url: String, homebrew_curl: T::Boolean).returns(T::Array[T::Hash[String, String]]) }
|
sig {
|
||||||
def self.page_headers(url, homebrew_curl: false)
|
params(
|
||||||
|
url: String,
|
||||||
|
url_options: T::Hash[Symbol, T.untyped],
|
||||||
|
homebrew_curl: T::Boolean,
|
||||||
|
).returns(T::Array[T::Hash[String, String]])
|
||||||
|
}
|
||||||
|
def self.page_headers(url, url_options: {}, homebrew_curl: false)
|
||||||
headers = []
|
headers = []
|
||||||
|
|
||||||
|
if url_options[:post_form].present? || url_options[:post_json].present?
|
||||||
|
curl_post_args = ["--request", "POST", *post_args(
|
||||||
|
post_form: url_options[:post_form],
|
||||||
|
post_json: url_options[:post_json],
|
||||||
|
)]
|
||||||
|
end
|
||||||
|
|
||||||
[:default, :browser].each do |user_agent|
|
[:default, :browser].each do |user_agent|
|
||||||
begin
|
begin
|
||||||
parsed_output = curl_headers(
|
parsed_output = curl_headers(
|
||||||
|
*curl_post_args,
|
||||||
"--max-redirs",
|
"--max-redirs",
|
||||||
MAX_REDIRECTIONS.to_s,
|
MAX_REDIRECTIONS.to_s,
|
||||||
url,
|
url,
|
||||||
@ -205,13 +244,28 @@ module Homebrew
|
|||||||
# array with the error message instead.
|
# array with the error message instead.
|
||||||
#
|
#
|
||||||
# @param url [String] the URL of the content to check
|
# @param url [String] the URL of the content to check
|
||||||
|
# @param url_options [Hash] options to modify curl behavior
|
||||||
# @param homebrew_curl [Boolean] whether to use brewed curl with the URL
|
# @param homebrew_curl [Boolean] whether to use brewed curl with the URL
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
sig { params(url: String, homebrew_curl: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
|
sig {
|
||||||
def self.page_content(url, homebrew_curl: false)
|
params(
|
||||||
|
url: String,
|
||||||
|
url_options: T::Hash[Symbol, T.untyped],
|
||||||
|
homebrew_curl: T::Boolean,
|
||||||
|
).returns(T::Hash[Symbol, T.untyped])
|
||||||
|
}
|
||||||
|
def self.page_content(url, url_options: {}, homebrew_curl: false)
|
||||||
|
if url_options[:post_form].present? || url_options[:post_json].present?
|
||||||
|
curl_post_args = ["--request", "POST", *post_args(
|
||||||
|
post_form: url_options[:post_form],
|
||||||
|
post_json: url_options[:post_json],
|
||||||
|
)]
|
||||||
|
end
|
||||||
|
|
||||||
stderr = T.let(nil, T.nilable(String))
|
stderr = T.let(nil, T.nilable(String))
|
||||||
[:default, :browser].each do |user_agent|
|
[:default, :browser].each do |user_agent|
|
||||||
stdout, stderr, status = curl_output(
|
stdout, stderr, status = curl_output(
|
||||||
|
*curl_post_args,
|
||||||
*PAGE_CONTENT_CURL_ARGS, url,
|
*PAGE_CONTENT_CURL_ARGS, url,
|
||||||
**DEFAULT_CURL_OPTIONS,
|
**DEFAULT_CURL_OPTIONS,
|
||||||
use_homebrew_curl: homebrew_curl || !curl_supports_fail_with_body?,
|
use_homebrew_curl: homebrew_curl || !curl_supports_fail_with_body?,
|
||||||
|
9
Library/Homebrew/livecheck/strategy.rbi
Normal file
9
Library/Homebrew/livecheck/strategy.rbi
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# typed: strict
|
||||||
|
|
||||||
|
module Homebrew
|
||||||
|
module Livecheck
|
||||||
|
module Strategy
|
||||||
|
include Kernel
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -81,11 +81,11 @@ module Homebrew
|
|||||||
regex: T.nilable(Regexp),
|
regex: T.nilable(Regexp),
|
||||||
provided_content: T.nilable(String),
|
provided_content: T.nilable(String),
|
||||||
homebrew_curl: T::Boolean,
|
homebrew_curl: T::Boolean,
|
||||||
_unused: T.untyped,
|
unused: T.untyped,
|
||||||
block: T.nilable(Proc),
|
block: T.nilable(Proc),
|
||||||
).returns(T::Hash[Symbol, T.untyped])
|
).returns(T::Hash[Symbol, T.untyped])
|
||||||
}
|
}
|
||||||
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **_unused, &block)
|
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **unused, &block)
|
||||||
match_data = { matches: {}, regex:, url: }
|
match_data = { matches: {}, regex:, url: }
|
||||||
match_data[:cached] = true if provided_content.is_a?(String)
|
match_data[:cached] = true if provided_content.is_a?(String)
|
||||||
|
|
||||||
@ -97,7 +97,13 @@ module Homebrew
|
|||||||
content = if provided_content
|
content = if provided_content
|
||||||
provided_content
|
provided_content
|
||||||
else
|
else
|
||||||
match_data.merge!(Strategy.page_content(match_data[:url], homebrew_curl:))
|
match_data.merge!(
|
||||||
|
Strategy.page_content(
|
||||||
|
match_data[:url],
|
||||||
|
url_options: unused.fetch(:url_options, {}),
|
||||||
|
homebrew_curl:,
|
||||||
|
),
|
||||||
|
)
|
||||||
match_data[:content]
|
match_data[:content]
|
||||||
end
|
end
|
||||||
return match_data unless content
|
return match_data unless content
|
||||||
|
@ -74,14 +74,18 @@ module Homebrew
|
|||||||
url: String,
|
url: String,
|
||||||
regex: T.nilable(Regexp),
|
regex: T.nilable(Regexp),
|
||||||
homebrew_curl: T::Boolean,
|
homebrew_curl: T::Boolean,
|
||||||
_unused: T.untyped,
|
unused: T.untyped,
|
||||||
block: T.nilable(Proc),
|
block: T.nilable(Proc),
|
||||||
).returns(T::Hash[Symbol, T.untyped])
|
).returns(T::Hash[Symbol, T.untyped])
|
||||||
}
|
}
|
||||||
def self.find_versions(url:, regex: nil, homebrew_curl: false, **_unused, &block)
|
def self.find_versions(url:, regex: nil, homebrew_curl: false, **unused, &block)
|
||||||
match_data = { matches: {}, regex:, url: }
|
match_data = { matches: {}, regex:, url: }
|
||||||
|
|
||||||
headers = Strategy.page_headers(url, homebrew_curl:)
|
headers = Strategy.page_headers(
|
||||||
|
url,
|
||||||
|
url_options: unused.fetch(:url_options, {}),
|
||||||
|
homebrew_curl:,
|
||||||
|
)
|
||||||
|
|
||||||
# Merge the headers from all responses into one hash
|
# Merge the headers from all responses into one hash
|
||||||
merged_headers = headers.reduce(&:merge)
|
merged_headers = headers.reduce(&:merge)
|
||||||
|
@ -102,11 +102,11 @@ module Homebrew
|
|||||||
regex: T.nilable(Regexp),
|
regex: T.nilable(Regexp),
|
||||||
provided_content: T.nilable(String),
|
provided_content: T.nilable(String),
|
||||||
homebrew_curl: T::Boolean,
|
homebrew_curl: T::Boolean,
|
||||||
_unused: T.untyped,
|
unused: T.untyped,
|
||||||
block: T.nilable(Proc),
|
block: T.nilable(Proc),
|
||||||
).returns(T::Hash[Symbol, T.untyped])
|
).returns(T::Hash[Symbol, T.untyped])
|
||||||
}
|
}
|
||||||
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **_unused, &block)
|
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **unused, &block)
|
||||||
raise ArgumentError, "#{Utils.demodulize(T.must(name))} requires a `strategy` block" if block.blank?
|
raise ArgumentError, "#{Utils.demodulize(T.must(name))} requires a `strategy` block" if block.blank?
|
||||||
|
|
||||||
match_data = { matches: {}, regex:, url: }
|
match_data = { matches: {}, regex:, url: }
|
||||||
@ -116,7 +116,13 @@ module Homebrew
|
|||||||
match_data[:cached] = true
|
match_data[:cached] = true
|
||||||
provided_content
|
provided_content
|
||||||
else
|
else
|
||||||
match_data.merge!(Strategy.page_content(url, homebrew_curl:))
|
match_data.merge!(
|
||||||
|
Strategy.page_content(
|
||||||
|
url,
|
||||||
|
url_options: unused.fetch(:url_options, {}),
|
||||||
|
homebrew_curl:,
|
||||||
|
),
|
||||||
|
)
|
||||||
match_data[:content]
|
match_data[:content]
|
||||||
end
|
end
|
||||||
return match_data if content.blank?
|
return match_data if content.blank?
|
||||||
|
@ -85,11 +85,11 @@ module Homebrew
|
|||||||
regex: T.nilable(Regexp),
|
regex: T.nilable(Regexp),
|
||||||
provided_content: T.nilable(String),
|
provided_content: T.nilable(String),
|
||||||
homebrew_curl: T::Boolean,
|
homebrew_curl: T::Boolean,
|
||||||
_unused: T.untyped,
|
unused: T.untyped,
|
||||||
block: T.nilable(Proc),
|
block: T.nilable(Proc),
|
||||||
).returns(T::Hash[Symbol, T.untyped])
|
).returns(T::Hash[Symbol, T.untyped])
|
||||||
}
|
}
|
||||||
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **_unused, &block)
|
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **unused, &block)
|
||||||
if regex.blank? && block.blank?
|
if regex.blank? && block.blank?
|
||||||
raise ArgumentError, "#{Utils.demodulize(T.must(name))} requires a regex or `strategy` block"
|
raise ArgumentError, "#{Utils.demodulize(T.must(name))} requires a regex or `strategy` block"
|
||||||
end
|
end
|
||||||
@ -101,7 +101,13 @@ module Homebrew
|
|||||||
match_data[:cached] = true
|
match_data[:cached] = true
|
||||||
provided_content
|
provided_content
|
||||||
else
|
else
|
||||||
match_data.merge!(Strategy.page_content(url, homebrew_curl:))
|
match_data.merge!(
|
||||||
|
Strategy.page_content(
|
||||||
|
url,
|
||||||
|
url_options: unused.fetch(:url_options, {}),
|
||||||
|
homebrew_curl:,
|
||||||
|
),
|
||||||
|
)
|
||||||
match_data[:content]
|
match_data[:content]
|
||||||
end
|
end
|
||||||
return match_data if content.blank?
|
return match_data if content.blank?
|
||||||
|
@ -217,13 +217,13 @@ module Homebrew
|
|||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
sig {
|
sig {
|
||||||
params(
|
params(
|
||||||
url: String,
|
url: String,
|
||||||
regex: T.nilable(Regexp),
|
regex: T.nilable(Regexp),
|
||||||
_unused: T.untyped,
|
unused: T.untyped,
|
||||||
block: T.nilable(Proc),
|
block: T.nilable(Proc),
|
||||||
).returns(T::Hash[Symbol, T.untyped])
|
).returns(T::Hash[Symbol, T.untyped])
|
||||||
}
|
}
|
||||||
def self.find_versions(url:, regex: nil, **_unused, &block)
|
def self.find_versions(url:, regex: nil, **unused, &block)
|
||||||
if regex.present? && block.blank?
|
if regex.present? && block.blank?
|
||||||
raise ArgumentError,
|
raise ArgumentError,
|
||||||
"#{Utils.demodulize(T.must(name))} only supports a regex when using a `strategy` block"
|
"#{Utils.demodulize(T.must(name))} only supports a regex when using a `strategy` block"
|
||||||
@ -231,7 +231,12 @@ module Homebrew
|
|||||||
|
|
||||||
match_data = { matches: {}, regex:, url: }
|
match_data = { matches: {}, regex:, url: }
|
||||||
|
|
||||||
match_data.merge!(Strategy.page_content(url))
|
match_data.merge!(
|
||||||
|
Strategy.page_content(
|
||||||
|
url,
|
||||||
|
url_options: unused.fetch(:url_options, {}),
|
||||||
|
),
|
||||||
|
)
|
||||||
content = match_data.delete(:content)
|
content = match_data.delete(:content)
|
||||||
return match_data if content.blank?
|
return match_data if content.blank?
|
||||||
|
|
||||||
|
@ -142,11 +142,11 @@ module Homebrew
|
|||||||
regex: T.nilable(Regexp),
|
regex: T.nilable(Regexp),
|
||||||
provided_content: T.nilable(String),
|
provided_content: T.nilable(String),
|
||||||
homebrew_curl: T::Boolean,
|
homebrew_curl: T::Boolean,
|
||||||
_unused: T.untyped,
|
unused: T.untyped,
|
||||||
block: T.nilable(Proc),
|
block: T.nilable(Proc),
|
||||||
).returns(T::Hash[Symbol, T.untyped])
|
).returns(T::Hash[Symbol, T.untyped])
|
||||||
}
|
}
|
||||||
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **_unused, &block)
|
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **unused, &block)
|
||||||
raise ArgumentError, "#{Utils.demodulize(T.must(name))} requires a `strategy` block" if block.blank?
|
raise ArgumentError, "#{Utils.demodulize(T.must(name))} requires a `strategy` block" if block.blank?
|
||||||
|
|
||||||
match_data = { matches: {}, regex:, url: }
|
match_data = { matches: {}, regex:, url: }
|
||||||
@ -156,7 +156,13 @@ module Homebrew
|
|||||||
match_data[:cached] = true
|
match_data[:cached] = true
|
||||||
provided_content
|
provided_content
|
||||||
else
|
else
|
||||||
match_data.merge!(Strategy.page_content(url, homebrew_curl:))
|
match_data.merge!(
|
||||||
|
Strategy.page_content(
|
||||||
|
url,
|
||||||
|
url_options: unused.fetch(:url_options, {}),
|
||||||
|
homebrew_curl:,
|
||||||
|
),
|
||||||
|
)
|
||||||
match_data[:content]
|
match_data[:content]
|
||||||
end
|
end
|
||||||
return match_data if content.blank?
|
return match_data if content.blank?
|
||||||
|
@ -102,11 +102,11 @@ module Homebrew
|
|||||||
regex: T.nilable(Regexp),
|
regex: T.nilable(Regexp),
|
||||||
provided_content: T.nilable(String),
|
provided_content: T.nilable(String),
|
||||||
homebrew_curl: T::Boolean,
|
homebrew_curl: T::Boolean,
|
||||||
_unused: T.untyped,
|
unused: T.untyped,
|
||||||
block: T.nilable(Proc),
|
block: T.nilable(Proc),
|
||||||
).returns(T::Hash[Symbol, T.untyped])
|
).returns(T::Hash[Symbol, T.untyped])
|
||||||
}
|
}
|
||||||
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **_unused, &block)
|
def self.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **unused, &block)
|
||||||
raise ArgumentError, "#{Utils.demodulize(T.must(name))} requires a `strategy` block" if block.blank?
|
raise ArgumentError, "#{Utils.demodulize(T.must(name))} requires a `strategy` block" if block.blank?
|
||||||
|
|
||||||
match_data = { matches: {}, regex:, url: }
|
match_data = { matches: {}, regex:, url: }
|
||||||
@ -116,7 +116,13 @@ module Homebrew
|
|||||||
match_data[:cached] = true
|
match_data[:cached] = true
|
||||||
provided_content
|
provided_content
|
||||||
else
|
else
|
||||||
match_data.merge!(Strategy.page_content(url, homebrew_curl:))
|
match_data.merge!(
|
||||||
|
Strategy.page_content(
|
||||||
|
url,
|
||||||
|
url_options: unused.fetch(:url_options, {}),
|
||||||
|
homebrew_curl:,
|
||||||
|
),
|
||||||
|
)
|
||||||
match_data[:content]
|
match_data[:content]
|
||||||
end
|
end
|
||||||
return match_data if content.blank?
|
return match_data if content.blank?
|
||||||
|
@ -5,6 +5,102 @@ require "livecheck/strategy"
|
|||||||
RSpec.describe Homebrew::Livecheck::Strategy do
|
RSpec.describe Homebrew::Livecheck::Strategy do
|
||||||
subject(:strategy) { described_class }
|
subject(:strategy) { described_class }
|
||||||
|
|
||||||
|
let(:url) { "https://brew.sh/" }
|
||||||
|
let(:redirection_url) { "https://brew.sh/redirection" }
|
||||||
|
|
||||||
|
let(:post_hash) do
|
||||||
|
{
|
||||||
|
"empty" => "",
|
||||||
|
"boolean" => "true",
|
||||||
|
"number" => "1",
|
||||||
|
"string" => "a + b = c",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:post_hash_symbol_keys) do
|
||||||
|
{
|
||||||
|
empty: "",
|
||||||
|
boolean: "true",
|
||||||
|
number: "1",
|
||||||
|
string: "a + b = c",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:form_string) { "empty=&boolean=true&number=1&string=a+%2B+b+%3D+c" }
|
||||||
|
let(:json_string) { '{"empty":"","boolean":"true","number":"1","string":"a + b = c"}' }
|
||||||
|
|
||||||
|
let(:response_hash) do
|
||||||
|
response_hash = {}
|
||||||
|
|
||||||
|
response_hash[:ok] = {
|
||||||
|
status_code: "200",
|
||||||
|
status_text: "OK",
|
||||||
|
headers: {
|
||||||
|
"cache-control" => "max-age=604800",
|
||||||
|
"content-type" => "text/html; charset=UTF-8",
|
||||||
|
"date" => "Wed, 1 Jan 2020 01:23:45 GMT",
|
||||||
|
"expires" => "Wed, 31 Jan 2020 01:23:45 GMT",
|
||||||
|
"last-modified" => "Thu, 1 Jan 2019 01:23:45 GMT",
|
||||||
|
"content-length" => "123",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response_hash[:redirection] = {
|
||||||
|
status_code: "301",
|
||||||
|
status_text: "Moved Permanently",
|
||||||
|
headers: {
|
||||||
|
"cache-control" => "max-age=604800",
|
||||||
|
"content-type" => "text/html; charset=UTF-8",
|
||||||
|
"date" => "Wed, 1 Jan 2020 01:23:45 GMT",
|
||||||
|
"expires" => "Wed, 31 Jan 2020 01:23:45 GMT",
|
||||||
|
"last-modified" => "Thu, 1 Jan 2019 01:23:45 GMT",
|
||||||
|
"content-length" => "123",
|
||||||
|
"location" => redirection_url,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response_hash
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:body) do
|
||||||
|
<<~HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Thank you!</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Download</h1>
|
||||||
|
<p>This download link could have been made publicly available in a reasonable fashion but we appreciate that you jumped through the hoops that we carefully set up!: <a href="https://brew.sh/example-1.2.3.tar.gz">Example v1.2.3</a></p>
|
||||||
|
<p>The current legacy version is: <a href="https://brew.sh/example-0.1.2.tar.gz">Example v0.1.2</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:response_text) do
|
||||||
|
response_text = {}
|
||||||
|
|
||||||
|
response_text[:ok] = <<~EOS
|
||||||
|
HTTP/1.1 #{response_hash[:ok][:status_code]} #{response_hash[:ok][:status_text]}\r
|
||||||
|
Cache-Control: #{response_hash[:ok][:headers]["cache-control"]}\r
|
||||||
|
Content-Type: #{response_hash[:ok][:headers]["content-type"]}\r
|
||||||
|
Date: #{response_hash[:ok][:headers]["date"]}\r
|
||||||
|
Expires: #{response_hash[:ok][:headers]["expires"]}\r
|
||||||
|
Last-Modified: #{response_hash[:ok][:headers]["last-modified"]}\r
|
||||||
|
Content-Length: #{response_hash[:ok][:headers]["content-length"]}\r
|
||||||
|
\r
|
||||||
|
#{body.rstrip}
|
||||||
|
EOS
|
||||||
|
|
||||||
|
response_text[:redirection_to_ok] = response_text[:ok].sub(
|
||||||
|
"HTTP/1.1 #{response_hash[:ok][:status_code]} #{response_hash[:ok][:status_text]}\r",
|
||||||
|
"HTTP/1.1 #{response_hash[:redirection][:status_code]} #{response_hash[:redirection][:status_text]}\r\n" \
|
||||||
|
"Location: #{response_hash[:redirection][:headers]["location"]}\r",
|
||||||
|
)
|
||||||
|
|
||||||
|
response_text
|
||||||
|
end
|
||||||
|
|
||||||
describe "::from_symbol" do
|
describe "::from_symbol" do
|
||||||
it "returns the Strategy module represented by the Symbol argument" do
|
it "returns the Strategy module represented by the Symbol argument" do
|
||||||
expect(strategy.from_symbol(:page_match)).to eq(Homebrew::Livecheck::Strategy::PageMatch)
|
expect(strategy.from_symbol(:page_match)).to eq(Homebrew::Livecheck::Strategy::PageMatch)
|
||||||
@ -30,6 +126,113 @@ RSpec.describe Homebrew::Livecheck::Strategy do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "::post_args" do
|
||||||
|
it "returns an array including `--data` and an encoded form data string" do
|
||||||
|
expect(strategy.post_args(post_form: post_hash)).to eq(["--data", form_string])
|
||||||
|
expect(strategy.post_args(post_form: post_hash_symbol_keys)).to eq(["--data", form_string])
|
||||||
|
|
||||||
|
# If both `post_form` and `post_json` are present, only `post_form` will
|
||||||
|
# be used.
|
||||||
|
expect(strategy.post_args(post_form: post_hash, post_json: post_hash)).to eq(["--data", form_string])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns an array including `--json` and a JSON string" do
|
||||||
|
expect(strategy.post_args(post_json: post_hash)).to eq(["--json", json_string])
|
||||||
|
expect(strategy.post_args(post_json: post_hash_symbol_keys)).to eq(["--json", json_string])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns an empty array if `post_form` value is blank" do
|
||||||
|
expect(strategy.post_args(post_form: {})).to eq([])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns an empty array if `post_json` value is blank" do
|
||||||
|
expect(strategy.post_args(post_json: {})).to eq([])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns an empty array if hash argument doesn't have a `post_form` or `post_json` value" do
|
||||||
|
expect(strategy.post_args).to eq([])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "::page_headers" do
|
||||||
|
let(:responses) { [response_hash[:ok]] }
|
||||||
|
|
||||||
|
it "returns headers from fetched content" do
|
||||||
|
allow(strategy).to receive(:curl_headers).and_return({ responses:, body: })
|
||||||
|
|
||||||
|
expect(strategy.page_headers(url)).to eq([responses.first[:headers]])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "handles `post_form` `url` options" do
|
||||||
|
allow(strategy).to receive(:curl_headers).and_return({ responses:, body: })
|
||||||
|
|
||||||
|
expect(strategy.page_headers(url, url_options: { post_form: post_hash }))
|
||||||
|
.to eq([responses.first[:headers]])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns an empty array if `curl_headers` only raises an `ErrorDuringExecution` error" do
|
||||||
|
allow(strategy).to receive(:curl_headers).and_raise(ErrorDuringExecution.new([], status: 1))
|
||||||
|
|
||||||
|
expect(strategy.page_headers(url)).to eq([])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "::page_content" do
|
||||||
|
let(:curl_version) { Version.new("8.7.1") }
|
||||||
|
let(:success_status) { instance_double(Process::Status, success?: true, exitstatus: 0) }
|
||||||
|
|
||||||
|
it "returns hash including fetched content" do
|
||||||
|
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(curl_version)
|
||||||
|
allow(strategy).to receive(:curl_output).and_return([response_text[:ok], nil, success_status])
|
||||||
|
|
||||||
|
expect(strategy.page_content(url)).to eq({ content: body })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "handles `post_form` `url` option" do
|
||||||
|
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(curl_version)
|
||||||
|
allow(strategy).to receive(:curl_output).and_return([response_text[:ok], nil, success_status])
|
||||||
|
|
||||||
|
expect(strategy.page_content(url, url_options: { post_form: post_hash })).to eq({ content: body })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "handles `post_json` `url` option" do
|
||||||
|
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(curl_version)
|
||||||
|
allow(strategy).to receive(:curl_output).and_return([response_text[:ok], nil, success_status])
|
||||||
|
|
||||||
|
expect(strategy.page_content(url, url_options: { post_json: post_hash })).to eq({ content: body })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns error `messages` from `stderr` in the return hash on failure when `stderr` is not `nil`" do
|
||||||
|
error_message = "curl: (6) Could not resolve host: brew.sh"
|
||||||
|
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(curl_version)
|
||||||
|
allow(strategy).to receive(:curl_output).and_return([
|
||||||
|
nil,
|
||||||
|
error_message,
|
||||||
|
instance_double(Process::Status, success?: false, exitstatus: 6),
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(strategy.page_content(url)).to eq({ messages: [error_message] })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns default error `messages` in the return hash on failure when `stderr` is `nil`" do
|
||||||
|
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(curl_version)
|
||||||
|
allow(strategy).to receive(:curl_output).and_return([
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
instance_double(Process::Status, success?: false, exitstatus: 1),
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(strategy.page_content(url)).to eq({ messages: ["cURL failed without a detectable error"] })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns hash including `final_url` if it differs from initial `url`" do
|
||||||
|
allow_any_instance_of(Utils::Curl).to receive(:curl_version).and_return(curl_version)
|
||||||
|
allow(strategy).to receive(:curl_output).and_return([response_text[:redirection_to_ok], nil, success_status])
|
||||||
|
|
||||||
|
expect(strategy.page_content(url)).to eq({ content: body, final_url: redirection_url })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "::handle_block_return" do
|
describe "::handle_block_return" do
|
||||||
it "returns an array of version strings when given a valid value" do
|
it "returns an array of version strings when given a valid value" do
|
||||||
expect(strategy.handle_block_return("1.2.3")).to eq(["1.2.3"])
|
expect(strategy.handle_block_return("1.2.3")).to eq(["1.2.3"])
|
||||||
|
@ -27,6 +27,15 @@ RSpec.describe Livecheck do
|
|||||||
end
|
end
|
||||||
let(:livecheck_c) { described_class.new(c) }
|
let(:livecheck_c) { described_class.new(c) }
|
||||||
|
|
||||||
|
let(:post_hash) do
|
||||||
|
{
|
||||||
|
"empty" => "",
|
||||||
|
"boolean" => "true",
|
||||||
|
"number" => "1",
|
||||||
|
"string" => "a + b = c",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
describe "#formula" do
|
describe "#formula" do
|
||||||
it "returns nil if not set" do
|
it "returns nil if not set" do
|
||||||
expect(livecheck_f.formula).to be_nil
|
expect(livecheck_f.formula).to be_nil
|
||||||
@ -137,25 +146,38 @@ RSpec.describe Livecheck do
|
|||||||
expect(livecheck_c.url).to eq(:url)
|
expect(livecheck_c.url).to eq(:url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "sets `url_options` when provided" do
|
||||||
|
post_args = { post_form: post_hash }
|
||||||
|
livecheck_f.url(url_string, **post_args)
|
||||||
|
expect(livecheck_f.url_options).to eq(post_args)
|
||||||
|
end
|
||||||
|
|
||||||
it "raises an ArgumentError if the argument isn't a valid Symbol" do
|
it "raises an ArgumentError if the argument isn't a valid Symbol" do
|
||||||
expect do
|
expect do
|
||||||
livecheck_f.url(:not_a_valid_symbol)
|
livecheck_f.url(:not_a_valid_symbol)
|
||||||
end.to raise_error ArgumentError
|
end.to raise_error ArgumentError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "raises an ArgumentError if both `post_form` and `post_json` arguments are provided" do
|
||||||
|
expect do
|
||||||
|
livecheck_f.url(:stable, post_form: post_hash, post_json: post_hash)
|
||||||
|
end.to raise_error ArgumentError
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#to_hash" do
|
describe "#to_hash" do
|
||||||
it "returns a Hash of all instance variables" do
|
it "returns a Hash of all instance variables" do
|
||||||
expect(livecheck_f.to_hash).to eq(
|
expect(livecheck_f.to_hash).to eq(
|
||||||
{
|
{
|
||||||
"cask" => nil,
|
"cask" => nil,
|
||||||
"formula" => nil,
|
"formula" => nil,
|
||||||
"regex" => nil,
|
"regex" => nil,
|
||||||
"skip" => false,
|
"skip" => false,
|
||||||
"skip_msg" => nil,
|
"skip_msg" => nil,
|
||||||
"strategy" => nil,
|
"strategy" => nil,
|
||||||
"throttle" => nil,
|
"throttle" => nil,
|
||||||
"url" => nil,
|
"url" => nil,
|
||||||
|
"url_options" => nil,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -112,6 +112,24 @@ end
|
|||||||
|
|
||||||
The referenced formula/cask should be in the same tap, as a reference to a formula/cask from another tap will generate an error if the user doesn't already have it tapped.
|
The referenced formula/cask should be in the same tap, as a reference to a formula/cask from another tap will generate an error if the user doesn't already have it tapped.
|
||||||
|
|
||||||
|
### `POST` requests
|
||||||
|
|
||||||
|
Some checks require making a `POST` request and that can be accomplished by adding a `post_form` or `post_json` option to a `livecheck` block `url`.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
livecheck do
|
||||||
|
url "https://example.com/download.php", post_form: {
|
||||||
|
"Name" => "",
|
||||||
|
"E-mail" => "",
|
||||||
|
}
|
||||||
|
regex(/href=.*?example[._-]v?(\d+(?:\.\d+)+)\.t/i)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
`post_form` is used for form data and `post_json` is used for JSON data. livecheck will encode the provided hash value to the appropriate format before making the request.
|
||||||
|
|
||||||
|
`POST` support only applies to strategies that use `Strategy::page_headers` or `::page_content` (directly or indirectly), so it does not apply to `ExtractPlist`, `Git`, `GithubLatest`, `GithubReleases`, etc.
|
||||||
|
|
||||||
### `strategy` blocks
|
### `strategy` blocks
|
||||||
|
|
||||||
If the upstream version format needs to be manipulated to match the formula/cask format, a `strategy` block can be used instead of a `regex`.
|
If the upstream version format needs to be manipulated to match the formula/cask format, a `strategy` block can be used instead of a `regex`.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user