Class: Homebrew::Livecheck::Strategy::Xml

Inherits:
Object
  • Object
show all
Defined in:
livecheck/strategy/xml.rb

Overview

The Xml strategy fetches content at a URL, parses it as XML using REXML, and provides the REXML::Document to a strategy block. If a regex is present in the livecheck block, it should be passed as the second argument to the strategy block.

This is a generic strategy that doesn’t contain any logic for finding versions, as the structure of XML data varies. Instead, a strategy block must be used to extract version information from the XML data. For more information on how to work with an REXML::Document object, please refer to the REXML::Document and REXML::Element documentation.

This strategy is not applied automatically and it is necessary to use strategy :xml in a livecheck block (in conjunction with a strategy block) to use it.

This strategy’s Xml.find_versions method can be used in other strategies that work with XML content, so it should only be necessary to write the version-finding logic that works with the parsed XML data.

Constant Summary collapse

NICE_NAME =
"XML"
PRIORITY =

A priority of zero causes livecheck to skip the strategy. We do this for Homebrew::Livecheck::Strategy::Xml so we can selectively apply it only when a strategy block is provided in a livecheck block.

0
URL_MATCH_REGEX =

The Regexp used to determine if the strategy applies to the URL.

%r{^https?://}i

Class Method Summary collapse

Class Method Details

.element_text(element, child_path = nil) ⇒ String?

Retrieves the stripped inner text of an REXML element. Returns nil if the optional child element doesn’t exist or the text is blank.

Parameters:

  • element (REXML::Element)

    an REXML element to retrieve text from, either directly or from a child element

  • child_path (String, nil) (defaults to: nil)

    the XPath of a child element to retrieve text from

Returns:



89
90
91
92
93
94
95
96
97
# File 'livecheck/strategy/xml.rb', line 89

def self.element_text(element, child_path = nil)
  element = element.get_elements(child_path).first if child_path.present?
  return if element.nil?

  text = element.text
  return if text.blank?

  text.strip
end

.find_versions(url:, regex: nil, provided_content: nil, homebrew_curl: false, **_unused, &block) ⇒ Hash{Symbol => T.untyped}

Checks the XML content at the URL for versions, using the provided strategy block to extract version information.

Parameters:

  • url (String)

    the URL of the content to check

  • regex (Regexp, nil) (defaults to: nil)

    a regex used for matching versions

  • provided_content (String, nil) (defaults to: nil)

    page content to use in place of fetching via Strategy#page_content

  • homebrew_curl (Boolean) (defaults to: false)

    whether to use brewed curl with the URL

  • _unused (T.untyped)
  • block (Proc, nil)

Returns:

Raises:

  • (ArgumentError)


149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'livecheck/strategy/xml.rb', line 149

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?

  match_data = { matches: {}, regex:, url: }
  return match_data if url.blank? || block.blank?

  content = if provided_content.is_a?(String)
    match_data[:cached] = true
    provided_content
  else
    match_data.merge!(Strategy.page_content(url, homebrew_curl:))
    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

.match?(url) ⇒ Boolean

Whether the strategy can be applied to the provided URL. Homebrew::Livecheck::Strategy::Xml will technically match any HTTP URL but is only usable with a livecheck block containing a strategy block.

Parameters:

  • url (String)

    the URL to match against

Returns:

  • (Boolean)


47
48
49
# File 'livecheck/strategy/xml.rb', line 47

def self.match?(url)
  URL_MATCH_REGEX.match?(url)
end

.parse_xml(content) ⇒ REXML::Document?

Parses XML text and returns an REXML::Document object.

Parameters:

  • content (String)

    the XML text to parse

Returns:

  • (REXML::Document, nil)


55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'livecheck/strategy/xml.rb', line 55

def self.parse_xml(content)
  parsing_tries = 0
  begin
    REXML::Document.new(content)
  rescue REXML::UndefinedNamespaceException => e
    undefined_prefix = e.to_s[/Undefined prefix ([^ ]+) found/i, 1]
    raise "Could not identify undefined prefix." if undefined_prefix.blank?

    # Only retry parsing once after removing prefix from content
    parsing_tries += 1
    raise "Could not parse XML after removing undefined prefix." if parsing_tries > 1

    # When an XML document contains a prefix without a corresponding
    # namespace, it's necessary to remove the prefix from the content
    # to be able to successfully parse it using REXML
    content = content.gsub(%r{(</?| )#{Regexp.escape(undefined_prefix)}:}, '\1')
    retry
  end
end

.versions_from_content(content, regex = nil, &block) ⇒ Array<String>

Parses XML text and identifies versions using a strategy block. If a regex is provided, it will be passed as the second argument to the strategy block (after the parsed XML data).

Parameters:

  • content (String)

    the XML text to parse and check

  • regex (Regexp, nil) (defaults to: nil)

    a regex used for matching versions in the content

  • block (Proc, nil)

Returns:



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'livecheck/strategy/xml.rb', line 113

def self.versions_from_content(content, regex = nil, &block)
  return [] if content.blank? || block.blank?

  require "rexml"
  xml = parse_xml(content)
  return [] if xml.blank?

  block_return_value = if regex.present?
    yield(xml, regex)
  elsif block.arity == 2
    raise "Two arguments found in `strategy` block but no regex provided."
  else
    yield(xml)
  end
  Strategy.handle_block_return(block_return_value)
end