123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494 |
- # :markup: markdown
- module LiquidInterpolatable
- extend ActiveSupport::Concern
- included do
- validate :validate_interpolation
- end
- def valid?(context = nil)
- super
- rescue Liquid::Error
- errors.empty?
- end
- def validate_interpolation
- interpolated
- rescue Liquid::ZeroDivisionError => e
- # Ignore error (likely due to possibly missing variables on "divided_by")
- rescue Liquid::Error => e
- errors.add(:options, "has an error with Liquid templating: #{e.message}")
- rescue
- # Calling `interpolated` without an incoming may naturally fail
- # with various errors when an agent expects one.
- end
- # Return the current interpolation context. Use this in your Agent
- # class to manipulate interpolation context for user.
- #
- # For example, to provide local variables:
- #
- # # Create a new scope to define variables in:
- # interpolation_context.stack {
- # interpolation_context['_something_'] = 42
- # # And user can say "{{_something_}}" in their options.
- # value = interpolated['some_key']
- # }
- #
- def interpolation_context
- @interpolation_context ||= Context.new(self)
- end
- # Take the given object as "self" in the current interpolation
- # context while running a given block.
- #
- # The most typical use case for this is to evaluate options for each
- # received event like this:
- #
- # def receive(incoming_events)
- # incoming_events.each do |event|
- # interpolate_with(event) do
- # # Handle each event based on "interpolated" options.
- # end
- # end
- # end
- def interpolate_with(self_object)
- case self_object
- when nil
- yield
- else
- context = interpolation_context
- begin
- context.environments.unshift(self_object.to_liquid)
- yield
- ensure
- context.environments.shift
- end
- end
- end
- def interpolate_with_each(array)
- array.each do |object|
- interpolate_with(object) do
- self.current_event = object
- yield object
- end
- end
- end
- def interpolate_options(options, self_object = nil)
- interpolate_with(self_object) do
- case options
- when String
- interpolate_string(options)
- when ActiveSupport::HashWithIndifferentAccess, Hash
- options.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) { |(key, value), memo|
- memo[key] = interpolate_options(value)
- }
- when Array
- options.map { |value| interpolate_options(value) }
- else
- options
- end
- end
- end
- def interpolated(self_object = nil)
- interpolate_with(self_object) do
- (@interpolated_cache ||= {})[[options, interpolation_context]] ||=
- interpolate_options(options)
- end
- end
- def interpolate_string(string, self_object = nil)
- interpolate_with(self_object) do
- catch :as_object do
- Liquid::Template.parse(string).render!(interpolation_context)
- end
- end
- end
- class Context < Liquid::Context
- def initialize(agent)
- super({}, {}, { agent: agent }, true)
- end
- def hash
- [@environments, @scopes, @registers].hash
- end
- def eql?(other)
- other.environments == @environments &&
- other.scopes == @scopes &&
- other.registers == @registers
- end
- end
- require 'uri'
- module Filters
- # Percent encoding for URI conforming to RFC 3986.
- # Ref: http://tools.ietf.org/html/rfc3986#page-12
- def uri_escape(string)
- CGI.escape(string) rescue string
- end
- # Parse an input into a URI object, optionally resolving it
- # against a base URI if given.
- #
- # A URI object will have the following properties: scheme,
- # userinfo, host, port, registry, path, opaque, query, and
- # fragment.
- def to_uri(uri, base_uri = nil)
- case base_uri
- when nil, ''
- Utils.normalize_uri(uri.to_s)
- else
- Utils.normalize_uri(base_uri) + Utils.normalize_uri(uri.to_s)
- end
- rescue URI::Error
- nil
- end
- # Get the destination URL of a given URL by recursively following
- # redirects, up to 5 times in a row. If a given string is not a
- # valid absolute HTTP URL or in case of too many redirects, the
- # original string is returned. If any network/protocol error
- # occurs while following redirects, the last URL followed is
- # returned.
- def uri_expand(url, limit = 5)
- case url
- when URI
- uri = url
- else
- url = url.to_s
- begin
- uri = Utils.normalize_uri(url)
- rescue URI::Error
- return url
- end
- end
- http = Faraday.new do |builder|
- builder.adapter :net_http
- # builder.use FaradayMiddleware::FollowRedirects, limit: limit
- # ...does not handle non-HTTP URLs.
- end
- limit.times do
- begin
- case uri
- when URI::HTTP
- return uri.to_s unless uri.host
- response = http.head(uri)
- case response.status
- when 301, 302, 303, 307
- if location = response['location']
- uri += Utils.normalize_uri(location)
- next
- end
- end
- end
- rescue URI::Error, Faraday::Error, SystemCallError => e
- logger.error "#{e.class} in #{__method__}(#{url.inspect}) [uri=#{uri.to_s.inspect}]: #{e.message}:\n#{e.backtrace.join("\n")}"
- end
- return uri.to_s
- end
- logger.error "Too many rediretions in #{__method__}(#{url.inspect}) [uri=#{uri.to_s.inspect}]"
- url
- end
- # Rebase URIs contained in attributes in a given HTML fragment
- def rebase_hrefs(input, base_uri)
- Utils.rebase_hrefs(input, base_uri) rescue input
- end
- # Unescape (basic) HTML entities in a string
- #
- # This currently decodes the following entities only: "'",
- # """, "<", ">", "&", "&#dd;" and "&#xhh;".
- def unescape(input)
- CGI.unescapeHTML(input) rescue input
- end
- # Escape a string for use in XPath expression
- def to_xpath(string)
- subs = string.to_s.scan(/\G(?:\A\z|[^"]+|[^']+)/).map { |x|
- case x
- when /"/
- %Q{'#{x}'}
- else
- %Q{"#{x}"}
- end
- }
- if subs.size == 1
- subs.first
- else
- 'concat(' << subs.join(', ') << ')'
- end
- end
- def regex_replace(input, regex, replacement = nil)
- input.to_s.gsub(Regexp.new(regex), unescape_replacement(replacement.to_s))
- end
- def regex_replace_first(input, regex, replacement = nil)
- input.to_s.sub(Regexp.new(regex), unescape_replacement(replacement.to_s))
- end
- # Serializes data as JSON
- def json(input)
- JSON.dump(input)
- end
- def md5(input)
- Digest::MD5.hexdigest(input.to_s)
- end
- def sha1(input)
- Digest::SHA1.hexdigest(input.to_s)
- end
- def sha256(input)
- Digest::SHA256.hexdigest(input.to_s)
- end
- def hmac_sha1(input, key)
- OpenSSL::HMAC.hexdigest('sha1', key.to_s, input.to_s)
- end
- def hmac_sha256(input, key)
- OpenSSL::HMAC.hexdigest('sha256', key.to_s, input.to_s)
- end
- # Returns a Ruby object
- #
- # It can be used as a JSONPath replacement for Agents that only support Liquid:
- #
- # Event: {"something": {"nested": {"data": 1}}}
- # Liquid: {{something.nested | as_object}}
- # Returns: {"data": 1}
- #
- # Splitting up a string with Liquid filters and return the Array:
- #
- # Event: {"data": "A,B,C"}}
- # Liquid: {{data | split: ',' | as_object}}
- # Returns: ['A', 'B', 'C']
- #
- # as_object ALWAYS has be the last filter in a Liquid expression!
- def as_object(object)
- throw :as_object, object.as_json
- end
- # Group an array of items by a property
- #
- # Example usage:
- #
- # {% assign posts_by_author = site.posts | group_by: "author" %}
- # {% for author in posts_by_author %}
- # <dt>{{author.name}}</dt>
- # {% for post in author.items %}
- # <dd><a href="{{post.url}}">{{post.title}}</a></dd>
- # {% endfor %}
- # {% endfor %}
- def group_by(input, property)
- if input.respond_to?(:group_by)
- input.group_by { |item| item[property] }.map do |value, items|
- { 'name' => value, 'items' => items }
- end
- else
- input
- end
- end
- private
- def logger
- @@logger ||=
- if defined?(Rails)
- Rails.logger
- else
- require 'logger'
- Logger.new(STDERR)
- end
- end
- BACKSLASH = "\\".freeze
- UNESCAPE = {
- "a" => "\a",
- "b" => "\b",
- "e" => "\e",
- "f" => "\f",
- "n" => "\n",
- "r" => "\r",
- "s" => " ",
- "t" => "\t",
- "v" => "\v",
- }
- # Unescape a replacement text for use in the second argument of
- # gsub/sub. The following escape sequences are recognized:
- #
- # - "\\" (backslash itself)
- # - "\a" (alert)
- # - "\b" (backspace)
- # - "\e" (escape)
- # - "\f" (form feed)
- # - "\n" (new line)
- # - "\r" (carriage return)
- # - "\s" (space)
- # - "\t" (horizontal tab)
- # - "\u{XXXX}" (unicode codepoint)
- # - "\v" (vertical tab)
- # - "\xXX" (hexadecimal character)
- # - "\1".."\9" (numbered capture groups)
- # - "\+" (last capture group)
- # - "\k<name>" (named capture group)
- # - "\&" or "\0" (complete matched text)
- # - "\`" (string before match)
- # - "\'" (string after match)
- #
- # Octal escape sequences are deliberately unsupported to avoid
- # conflict with numbered capture groups. Rather obscure Emacs
- # style character codes ("\C-x", "\M-\C-x" etc.) are also omitted
- # from this implementation.
- def unescape_replacement(s)
- s.gsub(/\\(?:([\d+&`'\\]|k<\w+>)|u\{([[:xdigit:]]+)\}|x([[:xdigit:]]{2})|(.))/) {
- if c = $1
- BACKSLASH + c
- elsif c = ($2 && [$2.to_i(16)].pack('U')) ||
- ($3 && [$3.to_i(16)].pack('C'))
- if c == BACKSLASH
- BACKSLASH + c
- else
- c
- end
- else
- UNESCAPE[$4] || $4
- end
- }
- end
- end
- Liquid::Template.register_filter(LiquidInterpolatable::Filters)
- module Tags
- class Credential < Liquid::Tag
- def initialize(tag_name, name, tokens)
- super
- @credential_name = name.strip
- end
- def render(context)
- context.registers[:agent].credential(@credential_name)
- end
- end
- class LineBreak < Liquid::Tag
- def render(context)
- "\n"
- end
- end
- end
- Liquid::Template.register_tag('credential', LiquidInterpolatable::Tags::Credential)
- Liquid::Template.register_tag('line_break', LiquidInterpolatable::Tags::LineBreak)
- module Blocks
- # Replace every occurrence of a given regex pattern in the first
- # "in" block with the result of the "with" block in which the
- # variable `match` is set for each iteration, which can be used as
- # follows:
- #
- # - `match[0]` or just `match`: the whole matching string
- # - `match[1]`..`match[n]`: strings matching the numbered capture groups
- # - `match.size`: total number of the elements above (n+1)
- # - `match.names`: array of names of named capture groups
- # - `match[name]`..: strings matching the named capture groups
- # - `match.pre_match`: string preceding the match
- # - `match.post_match`: string following the match
- # - `match.***`: equivalent to `match['***']` unless it conflicts with the existing methods above
- #
- # If named captures (`(?<name>...)`) are used in the pattern, they
- # are also made accessible as variables. Note that if numbered
- # captures are used mixed with named captures, you could get
- # unexpected results.
- #
- # Example usage:
- #
- # {% regex_replace "\w+" in %}Use me like this.{% with %}{{ match | capitalize }}{% endregex_replace %}
- # {% assign fullname = "Doe, John A." %}
- # {% regex_replace_first "\A(?<name1>.+), (?<name2>.+)\z" in %}{{ fullname }}{% with %}{{ name2 }} {{ name1 }}{% endregex_replace_first %}
- #
- # Use Me Like This.
- #
- # John A. Doe
- #
- class RegexReplace < Liquid::Block
- Syntax = /\A\s*(#{Liquid::QuotedFragment})(?:\s+in)?\s*\z/
- def initialize(tag_name, markup, tokens)
- super
- case markup
- when Syntax
- @regexp = $1
- else
- raise Liquid::SyntaxError, 'Syntax Error in regex_replace tag - Valid syntax: regex_replace pattern in'
- end
- @in_block = Liquid::BlockBody.new
- @with_block = nil
- end
- def parse(tokens)
- if more = parse_body(@in_block, tokens)
- @with_block = Liquid::BlockBody.new
- parse_body(@with_block, tokens)
- end
- end
- def nodelist
- if @with_block
- [@in_block, @with_block]
- else
- [@in_block]
- end
- end
- def unknown_tag(tag, markup, tokens)
- return super unless tag == 'with'.freeze
- @with_block = Liquid::BlockBody.new
- end
- def render(context)
- begin
- regexp = Regexp.new(context[@regexp].to_s)
- rescue ::SyntaxError => e
- raise Liquid::SyntaxError, "Syntax Error in regex_replace tag - #{e.message}"
- end
- subject = @in_block.render(context)
- subject.send(first? ? :sub : :gsub, regexp) {
- next '' unless @with_block
- m = Regexp.last_match
- context.stack do
- m.names.each do |name|
- context[name] = m[name]
- end
- context['match'.freeze] = m
- @with_block.render(context)
- end
- }
- end
- def first?
- @tag_name.end_with?('_first'.freeze)
- end
- end
- end
- Liquid::Template.register_tag('regex_replace', LiquidInterpolatable::Blocks::RegexReplace)
- Liquid::Template.register_tag('regex_replace_first', LiquidInterpolatable::Blocks::RegexReplace)
- end
|