liquid_interpolatable.rb 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. module LiquidInterpolatable
  2. extend ActiveSupport::Concern
  3. included do
  4. validate :validate_interpolation
  5. end
  6. def valid?(context = nil)
  7. super
  8. rescue Liquid::Error
  9. errors.empty?
  10. end
  11. def validate_interpolation
  12. interpolated
  13. rescue Liquid::Error => e
  14. errors.add(:options, "has an error with Liquid templating: #{e.message}")
  15. rescue
  16. # Calling `interpolated` without an incoming may naturally fail
  17. # with various errors when an agent expects one.
  18. end
  19. # Return the current interpolation context. Use this in your Agent
  20. # class to manipulate interpolation context for user.
  21. #
  22. # For example, to provide local variables:
  23. #
  24. # # Create a new scope to define variables in:
  25. # interpolation_context.stack {
  26. # interpolation_context['_something_'] = 42
  27. # # And user can say "{{_something_}}" in their options.
  28. # value = interpolated['some_key']
  29. # }
  30. #
  31. def interpolation_context
  32. @interpolation_context ||= Context.new(self)
  33. end
  34. # Take the given object as "self" in the current interpolation
  35. # context while running a given block.
  36. #
  37. # The most typical use case for this is to evaluate options for each
  38. # received event like this:
  39. #
  40. # def receive(incoming_events)
  41. # incoming_events.each do |event|
  42. # interpolate_with(event) do
  43. # # Handle each event based on "interpolated" options.
  44. # end
  45. # end
  46. # end
  47. def interpolate_with(self_object)
  48. case self_object
  49. when nil
  50. yield
  51. else
  52. context = interpolation_context
  53. begin
  54. context.environments.unshift(self_object.to_liquid)
  55. yield
  56. ensure
  57. context.environments.shift
  58. end
  59. end
  60. end
  61. def interpolate_options(options, self_object = nil)
  62. interpolate_with(self_object) do
  63. case options
  64. when String
  65. interpolate_string(options)
  66. when ActiveSupport::HashWithIndifferentAccess, Hash
  67. options.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) { |(key, value), memo|
  68. memo[key] = interpolate_options(value)
  69. }
  70. when Array
  71. options.map { |value| interpolate_options(value) }
  72. else
  73. options
  74. end
  75. end
  76. end
  77. def interpolated(self_object = nil)
  78. interpolate_with(self_object) do
  79. (@interpolated_cache ||= {})[[options, interpolation_context]] ||=
  80. interpolate_options(options)
  81. end
  82. end
  83. def interpolate_string(string, self_object = nil)
  84. interpolate_with(self_object) do
  85. Liquid::Template.parse(string).render!(interpolation_context)
  86. end
  87. end
  88. class Context < Liquid::Context
  89. def initialize(agent)
  90. super({}, {}, { agent: agent }, true)
  91. end
  92. def hash
  93. [@environments, @scopes, @registers].hash
  94. end
  95. def eql?(other)
  96. other.environments == @environments &&
  97. other.scopes == @scopes &&
  98. other.registers == @registers
  99. end
  100. end
  101. require 'uri'
  102. module Filters
  103. # Percent encoding for URI conforming to RFC 3986.
  104. # Ref: http://tools.ietf.org/html/rfc3986#page-12
  105. def uri_escape(string)
  106. CGI.escape(string) rescue string
  107. end
  108. # Parse an input into a URI object, optionally resolving it
  109. # against a base URI if given.
  110. #
  111. # A URI object will have the following properties: scheme,
  112. # userinfo, host, port, registry, path, opaque, query, and
  113. # fragment.
  114. def to_uri(uri, base_uri = nil)
  115. if base_uri
  116. URI(base_uri) + uri.to_s
  117. else
  118. URI(uri.to_s)
  119. end
  120. rescue URI::Error
  121. nil
  122. end
  123. # Get the destination URL of a given URL by recursively following
  124. # redirects, up to 5 times in a row. If a given string is not a
  125. # valid absolute HTTP URL or in case of too many redirects, the
  126. # original string is returned. If any network/protocol error
  127. # occurs while following redirects, the last URL followed is
  128. # returned.
  129. def uri_expand(url, limit = 5)
  130. case url
  131. when URI
  132. uri = url
  133. else
  134. url = url.to_s
  135. begin
  136. uri = URI(url)
  137. rescue URI::Error
  138. return url
  139. end
  140. end
  141. http = Faraday.new do |builder|
  142. builder.adapter :net_http
  143. # builder.use FaradayMiddleware::FollowRedirects, limit: limit
  144. # ...does not handle non-HTTP URLs.
  145. end
  146. limit.times do
  147. begin
  148. case uri
  149. when URI::HTTP
  150. return uri.to_s unless uri.host
  151. response = http.head(uri)
  152. case response.status
  153. when 301, 302, 303, 307
  154. if location = response['location']
  155. uri += location
  156. next
  157. end
  158. end
  159. end
  160. rescue URI::Error, Faraday::Error, SystemCallError => e
  161. logger.error "#{e.class} in #{__method__}(#{url.inspect}) [uri=#{uri.to_s.inspect}]: #{e.message}:\n#{e.backtrace.join("\n")}"
  162. end
  163. return uri.to_s
  164. end
  165. logger.error "Too many rediretions in #{__method__}(#{url.inspect}) [uri=#{uri.to_s.inspect}]"
  166. url
  167. end
  168. # Unescape (basic) HTML entities in a string
  169. #
  170. # This currently decodes the following entities only: "&apos;",
  171. # "&quot;", "&lt;", "&gt;", "&amp;", "&#dd;" and "&#xhh;".
  172. def unescape(input)
  173. CGI.unescapeHTML(input) rescue input
  174. end
  175. # Escape a string for use in XPath expression
  176. def to_xpath(string)
  177. subs = string.to_s.scan(/\G(?:\A\z|[^"]+|[^']+)/).map { |x|
  178. case x
  179. when /"/
  180. %Q{'#{x}'}
  181. else
  182. %Q{"#{x}"}
  183. end
  184. }
  185. if subs.size == 1
  186. subs.first
  187. else
  188. 'concat(' << subs.join(', ') << ')'
  189. end
  190. end
  191. def regex_replace(input, regex, replacement = nil)
  192. input.to_s.gsub(Regexp.new(regex), unescape_replacement(replacement.to_s))
  193. end
  194. def regex_replace_first(input, regex, replacement = nil)
  195. input.to_s.sub(Regexp.new(regex), unescape_replacement(replacement.to_s))
  196. end
  197. private
  198. def logger
  199. @@logger ||=
  200. if defined?(Rails)
  201. Rails.logger
  202. else
  203. require 'logger'
  204. Logger.new(STDERR)
  205. end
  206. end
  207. BACKSLASH = "\\".freeze
  208. UNESCAPE = {
  209. "a" => "\a",
  210. "b" => "\b",
  211. "e" => "\e",
  212. "f" => "\f",
  213. "n" => "\n",
  214. "r" => "\r",
  215. "s" => " ",
  216. "t" => "\t",
  217. "v" => "\v",
  218. }
  219. # Unescape a replacement text for use in the second argument of
  220. # gsub/sub. The following escape sequences are recognized:
  221. #
  222. # - "\\" (backslash itself)
  223. # - "\a" (alert)
  224. # - "\b" (backspace)
  225. # - "\e" (escape)
  226. # - "\f" (form feed)
  227. # - "\n" (new line)
  228. # - "\r" (carriage return)
  229. # - "\s" (space)
  230. # - "\t" (horizontal tab)
  231. # - "\u{XXXX}" (unicode codepoint)
  232. # - "\v" (vertical tab)
  233. # - "\xXX" (hexadecimal character)
  234. # - "\1".."\9" (numbered capture groups)
  235. # - "\+" (last capture group)
  236. # - "\k<name>" (named capture group)
  237. # - "\&" or "\0" (complete matched text)
  238. # - "\`" (string before match)
  239. # - "\'" (string after match)
  240. #
  241. # Octal escape sequences are deliberately unsupported to avoid
  242. # conflict with numbered capture groups. Rather obscure Emacs
  243. # style character codes ("\C-x", "\M-\C-x" etc.) are also omitted
  244. # from this implementation.
  245. def unescape_replacement(s)
  246. s.gsub(/\\(?:([\d+&`'\\]|k<\w+>)|u\{([[:xdigit:]]+)\}|x([[:xdigit:]]{2})|(.))/) {
  247. if c = $1
  248. BACKSLASH + c
  249. elsif c = ($2 && [$2.to_i(16)].pack('U')) ||
  250. ($3 && [$3.to_i(16)].pack('C'))
  251. if c == BACKSLASH
  252. BACKSLASH + c
  253. else
  254. c
  255. end
  256. else
  257. UNESCAPE[$4] || $4
  258. end
  259. }
  260. end
  261. end
  262. Liquid::Template.register_filter(LiquidInterpolatable::Filters)
  263. module Tags
  264. class Credential < Liquid::Tag
  265. def initialize(tag_name, name, tokens)
  266. super
  267. @credential_name = name.strip
  268. end
  269. def render(context)
  270. credential = context.registers[:agent].credential(@credential_name)
  271. raise "No user credential named '#{@credential_name}' defined" if credential.nil?
  272. credential
  273. end
  274. end
  275. class LineBreak < Liquid::Tag
  276. def render(context)
  277. "\n"
  278. end
  279. end
  280. end
  281. Liquid::Template.register_tag('credential', LiquidInterpolatable::Tags::Credential)
  282. Liquid::Template.register_tag('line_break', LiquidInterpolatable::Tags::LineBreak)
  283. end