liquid_interpolatable.rb 6.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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. # Escape a string for use in XPath expression
  169. def to_xpath(string)
  170. subs = string.to_s.scan(/\G(?:\A\z|[^"]+|[^']+)/).map { |x|
  171. case x
  172. when /"/
  173. %Q{'#{x}'}
  174. else
  175. %Q{"#{x}"}
  176. end
  177. }
  178. if subs.size == 1
  179. subs.first
  180. else
  181. 'concat(' << subs.join(', ') << ')'
  182. end
  183. end
  184. def regex_replace(input, regex, replacement = ''.freeze)
  185. input.to_s.gsub(Regexp.new(regex), replacement.to_s)
  186. end
  187. def regex_replace_first(input, regex, replacement = ''.freeze)
  188. input.to_s.sub(Regexp.new(regex), replacement.to_s)
  189. end
  190. private
  191. def logger
  192. @@logger ||=
  193. if defined?(Rails)
  194. Rails.logger
  195. else
  196. require 'logger'
  197. Logger.new(STDERR)
  198. end
  199. end
  200. end
  201. Liquid::Template.register_filter(LiquidInterpolatable::Filters)
  202. module Tags
  203. class Credential < Liquid::Tag
  204. def initialize(tag_name, name, tokens)
  205. super
  206. @credential_name = name.strip
  207. end
  208. def render(context)
  209. credential = context.registers[:agent].credential(@credential_name)
  210. raise "No user credential named '#{@credential_name}' defined" if credential.nil?
  211. credential
  212. end
  213. end
  214. class LineBreak < Liquid::Tag
  215. def render(context)
  216. "\n"
  217. end
  218. end
  219. end
  220. Liquid::Template.register_tag('credential', LiquidInterpolatable::Tags::Credential)
  221. Liquid::Template.register_tag('line_break', LiquidInterpolatable::Tags::LineBreak)
  222. end