liquid_interpolatable.rb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. # :markup: markdown
  2. module LiquidInterpolatable
  3. extend ActiveSupport::Concern
  4. included do
  5. validate :validate_interpolation
  6. end
  7. def valid?(context = nil)
  8. super
  9. rescue Liquid::Error
  10. errors.empty?
  11. end
  12. def validate_interpolation
  13. interpolated
  14. rescue Liquid::ZeroDivisionError => e
  15. # Ignore error (likely due to possibly missing variables on "divided_by")
  16. rescue Liquid::Error => e
  17. errors.add(:options, "has an error with Liquid templating: #{e.message}")
  18. rescue
  19. # Calling `interpolated` without an incoming may naturally fail
  20. # with various errors when an agent expects one.
  21. end
  22. # Return the current interpolation context. Use this in your Agent
  23. # class to manipulate interpolation context for user.
  24. #
  25. # For example, to provide local variables:
  26. #
  27. # # Create a new scope to define variables in:
  28. # interpolation_context.stack {
  29. # interpolation_context['_something_'] = 42
  30. # # And user can say "{{_something_}}" in their options.
  31. # value = interpolated['some_key']
  32. # }
  33. #
  34. def interpolation_context
  35. @interpolation_context ||= Context.new(self)
  36. end
  37. # Take the given object as "self" in the current interpolation
  38. # context while running a given block.
  39. #
  40. # The most typical use case for this is to evaluate options for each
  41. # received event like this:
  42. #
  43. # def receive(incoming_events)
  44. # incoming_events.each do |event|
  45. # interpolate_with(event) do
  46. # # Handle each event based on "interpolated" options.
  47. # end
  48. # end
  49. # end
  50. def interpolate_with(self_object)
  51. case self_object
  52. when nil
  53. yield
  54. else
  55. context = interpolation_context
  56. begin
  57. context.environments.unshift(self_object.to_liquid)
  58. yield
  59. ensure
  60. context.environments.shift
  61. end
  62. end
  63. end
  64. def interpolate_with_each(array)
  65. array.each do |object|
  66. interpolate_with(object) do
  67. yield object
  68. end
  69. end
  70. end
  71. def interpolate_options(options, self_object = nil)
  72. interpolate_with(self_object) do
  73. case options
  74. when String
  75. interpolate_string(options)
  76. when ActiveSupport::HashWithIndifferentAccess, Hash
  77. options.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) { |(key, value), memo|
  78. memo[key] = interpolate_options(value)
  79. }
  80. when Array
  81. options.map { |value| interpolate_options(value) }
  82. else
  83. options
  84. end
  85. end
  86. end
  87. def interpolated(self_object = nil)
  88. interpolate_with(self_object) do
  89. (@interpolated_cache ||= {})[[options, interpolation_context]] ||=
  90. interpolate_options(options)
  91. end
  92. end
  93. def interpolate_string(string, self_object = nil)
  94. interpolate_with(self_object) do
  95. catch :as_object do
  96. Liquid::Template.parse(string).render!(interpolation_context)
  97. end
  98. end
  99. end
  100. class Context < Liquid::Context
  101. def initialize(agent)
  102. super({}, {}, { agent: agent }, true)
  103. end
  104. def hash
  105. [@environments, @scopes, @registers].hash
  106. end
  107. def eql?(other)
  108. other.environments == @environments &&
  109. other.scopes == @scopes &&
  110. other.registers == @registers
  111. end
  112. end
  113. require 'uri'
  114. module Filters
  115. # Percent encoding for URI conforming to RFC 3986.
  116. # Ref: http://tools.ietf.org/html/rfc3986#page-12
  117. def uri_escape(string)
  118. CGI.escape(string) rescue string
  119. end
  120. # Parse an input into a URI object, optionally resolving it
  121. # against a base URI if given.
  122. #
  123. # A URI object will have the following properties: scheme,
  124. # userinfo, host, port, registry, path, opaque, query, and
  125. # fragment.
  126. def to_uri(uri, base_uri = nil)
  127. case base_uri
  128. when nil, ''
  129. Utils.normalize_uri(uri.to_s)
  130. else
  131. Utils.normalize_uri(base_uri) + Utils.normalize_uri(uri.to_s)
  132. end
  133. rescue URI::Error
  134. nil
  135. end
  136. # Get the destination URL of a given URL by recursively following
  137. # redirects, up to 5 times in a row. If a given string is not a
  138. # valid absolute HTTP URL or in case of too many redirects, the
  139. # original string is returned. If any network/protocol error
  140. # occurs while following redirects, the last URL followed is
  141. # returned.
  142. def uri_expand(url, limit = 5)
  143. case url
  144. when URI
  145. uri = url
  146. else
  147. url = url.to_s
  148. begin
  149. uri = Utils.normalize_uri(url)
  150. rescue URI::Error
  151. return url
  152. end
  153. end
  154. http = Faraday.new do |builder|
  155. builder.adapter :net_http
  156. # builder.use FaradayMiddleware::FollowRedirects, limit: limit
  157. # ...does not handle non-HTTP URLs.
  158. end
  159. limit.times do
  160. begin
  161. case uri
  162. when URI::HTTP
  163. return uri.to_s unless uri.host
  164. response = http.head(uri)
  165. case response.status
  166. when 301, 302, 303, 307
  167. if location = response['location']
  168. uri += Utils.normalize_uri(location)
  169. next
  170. end
  171. end
  172. end
  173. rescue URI::Error, Faraday::Error, SystemCallError => e
  174. logger.error "#{e.class} in #{__method__}(#{url.inspect}) [uri=#{uri.to_s.inspect}]: #{e.message}:\n#{e.backtrace.join("\n")}"
  175. end
  176. return uri.to_s
  177. end
  178. logger.error "Too many rediretions in #{__method__}(#{url.inspect}) [uri=#{uri.to_s.inspect}]"
  179. url
  180. end
  181. # Rebase URIs contained in attributes in a given HTML fragment
  182. def rebase_hrefs(input, base_uri)
  183. Utils.rebase_hrefs(input, base_uri) rescue input
  184. end
  185. # Unescape (basic) HTML entities in a string
  186. #
  187. # This currently decodes the following entities only: "&apos;",
  188. # "&quot;", "&lt;", "&gt;", "&amp;", "&#dd;" and "&#xhh;".
  189. def unescape(input)
  190. CGI.unescapeHTML(input) rescue input
  191. end
  192. # Escape a string for use in XPath expression
  193. def to_xpath(string)
  194. subs = string.to_s.scan(/\G(?:\A\z|[^"]+|[^']+)/).map { |x|
  195. case x
  196. when /"/
  197. %Q{'#{x}'}
  198. else
  199. %Q{"#{x}"}
  200. end
  201. }
  202. if subs.size == 1
  203. subs.first
  204. else
  205. 'concat(' << subs.join(', ') << ')'
  206. end
  207. end
  208. def regex_replace(input, regex, replacement = nil)
  209. input.to_s.gsub(Regexp.new(regex), unescape_replacement(replacement.to_s))
  210. end
  211. def regex_replace_first(input, regex, replacement = nil)
  212. input.to_s.sub(Regexp.new(regex), unescape_replacement(replacement.to_s))
  213. end
  214. # Serializes data as JSON
  215. def json(input)
  216. JSON.dump(input)
  217. end
  218. # Returns a Ruby object
  219. #
  220. # It can be used as a JSONPath replacement for Agents that only support Liquid:
  221. #
  222. # Event: {"something": {"nested": {"data": 1}}}
  223. # Liquid: {{something.nested | as_object}}
  224. # Returns: {"data": 1}
  225. #
  226. # Splitting up a string with Liquid filters and return the Array:
  227. #
  228. # Event: {"data": "A,B,C"}}
  229. # Liquid: {{data | split: ',' | as_object}}
  230. # Returns: ['A', 'B', 'C']
  231. #
  232. # as_object ALWAYS has be the last filter in a Liquid expression!
  233. def as_object(object)
  234. throw :as_object, object.as_json
  235. end
  236. private
  237. def logger
  238. @@logger ||=
  239. if defined?(Rails)
  240. Rails.logger
  241. else
  242. require 'logger'
  243. Logger.new(STDERR)
  244. end
  245. end
  246. BACKSLASH = "\\".freeze
  247. UNESCAPE = {
  248. "a" => "\a",
  249. "b" => "\b",
  250. "e" => "\e",
  251. "f" => "\f",
  252. "n" => "\n",
  253. "r" => "\r",
  254. "s" => " ",
  255. "t" => "\t",
  256. "v" => "\v",
  257. }
  258. # Unescape a replacement text for use in the second argument of
  259. # gsub/sub. The following escape sequences are recognized:
  260. #
  261. # - "\\" (backslash itself)
  262. # - "\a" (alert)
  263. # - "\b" (backspace)
  264. # - "\e" (escape)
  265. # - "\f" (form feed)
  266. # - "\n" (new line)
  267. # - "\r" (carriage return)
  268. # - "\s" (space)
  269. # - "\t" (horizontal tab)
  270. # - "\u{XXXX}" (unicode codepoint)
  271. # - "\v" (vertical tab)
  272. # - "\xXX" (hexadecimal character)
  273. # - "\1".."\9" (numbered capture groups)
  274. # - "\+" (last capture group)
  275. # - "\k<name>" (named capture group)
  276. # - "\&" or "\0" (complete matched text)
  277. # - "\`" (string before match)
  278. # - "\'" (string after match)
  279. #
  280. # Octal escape sequences are deliberately unsupported to avoid
  281. # conflict with numbered capture groups. Rather obscure Emacs
  282. # style character codes ("\C-x", "\M-\C-x" etc.) are also omitted
  283. # from this implementation.
  284. def unescape_replacement(s)
  285. s.gsub(/\\(?:([\d+&`'\\]|k<\w+>)|u\{([[:xdigit:]]+)\}|x([[:xdigit:]]{2})|(.))/) {
  286. if c = $1
  287. BACKSLASH + c
  288. elsif c = ($2 && [$2.to_i(16)].pack('U')) ||
  289. ($3 && [$3.to_i(16)].pack('C'))
  290. if c == BACKSLASH
  291. BACKSLASH + c
  292. else
  293. c
  294. end
  295. else
  296. UNESCAPE[$4] || $4
  297. end
  298. }
  299. end
  300. end
  301. Liquid::Template.register_filter(LiquidInterpolatable::Filters)
  302. module Tags
  303. class Credential < Liquid::Tag
  304. def initialize(tag_name, name, tokens)
  305. super
  306. @credential_name = name.strip
  307. end
  308. def render(context)
  309. context.registers[:agent].credential(@credential_name)
  310. end
  311. end
  312. class LineBreak < Liquid::Tag
  313. def render(context)
  314. "\n"
  315. end
  316. end
  317. end
  318. Liquid::Template.register_tag('credential', LiquidInterpolatable::Tags::Credential)
  319. Liquid::Template.register_tag('line_break', LiquidInterpolatable::Tags::LineBreak)
  320. module Blocks
  321. # Replace every occurrence of a given regex pattern in the first
  322. # "in" block with the result of the "with" block in which the
  323. # variable `match` is set for each iteration, which can be used as
  324. # follows:
  325. #
  326. # - `match[0]` or just `match`: the whole matching string
  327. # - `match[1]`..`match[n]`: strings matching the numbered capture groups
  328. # - `match.size`: total number of the elements above (n+1)
  329. # - `match.names`: array of names of named capture groups
  330. # - `match[name]`..: strings matching the named capture groups
  331. # - `match.pre_match`: string preceding the match
  332. # - `match.post_match`: string following the match
  333. # - `match.***`: equivalent to `match['***']` unless it conflicts with the existing methods above
  334. #
  335. # If named captures (`(?<name>...)`) are used in the pattern, they
  336. # are also made accessible as variables. Note that if numbered
  337. # captures are used mixed with named captures, you could get
  338. # unexpected results.
  339. #
  340. # Example usage:
  341. #
  342. # {% regex_replace "\w+" in %}Use me like this.{% with %}{{ match | capitalize }}{% endregex_replace %}
  343. # {% assign fullname = "Doe, John A." %}
  344. # {% regex_replace_first "\A(?<name1>.+), (?<name2>.+)\z" in %}{{ fullname }}{% with %}{{ name2 }} {{ name1 }}{% endregex_replace_first %}
  345. #
  346. # Use Me Like This.
  347. #
  348. # John A. Doe
  349. #
  350. class RegexReplace < Liquid::Block
  351. Syntax = /\A\s*(#{Liquid::QuotedFragment})(?:\s+in)?\s*\z/
  352. def initialize(tag_name, markup, tokens)
  353. super
  354. case markup
  355. when Syntax
  356. @regexp = $1
  357. else
  358. raise Liquid::SyntaxError, 'Syntax Error in regex_replace tag - Valid syntax: regex_replace pattern in'
  359. end
  360. @in_block = Liquid::BlockBody.new
  361. @with_block = nil
  362. end
  363. def parse(tokens)
  364. if more = parse_body(@in_block, tokens)
  365. @with_block = Liquid::BlockBody.new
  366. parse_body(@with_block, tokens)
  367. end
  368. end
  369. def nodelist
  370. if @with_block
  371. [@in_block, @with_block]
  372. else
  373. [@in_block]
  374. end
  375. end
  376. def unknown_tag(tag, markup, tokens)
  377. return super unless tag == 'with'.freeze
  378. @with_block = Liquid::BlockBody.new
  379. end
  380. def render(context)
  381. begin
  382. regexp = Regexp.new(context[@regexp].to_s)
  383. rescue ::SyntaxError => e
  384. raise Liquid::SyntaxError, "Syntax Error in regex_replace tag - #{e.message}"
  385. end
  386. subject = @in_block.render(context)
  387. subject.send(first? ? :sub : :gsub, regexp) {
  388. next '' unless @with_block
  389. m = Regexp.last_match
  390. context.stack do
  391. m.names.each do |name|
  392. context[name] = m[name]
  393. end
  394. context['match'.freeze] = m
  395. @with_block.render(context)
  396. end
  397. }
  398. end
  399. def first?
  400. @tag_name.end_with?('_first'.freeze)
  401. end
  402. end
  403. end
  404. Liquid::Template.register_tag('regex_replace', LiquidInterpolatable::Blocks::RegexReplace)
  405. Liquid::Template.register_tag('regex_replace_first', LiquidInterpolatable::Blocks::RegexReplace)
  406. end