1
0
Эх сурвалжийг харах

Add as_object Liquid filter

The `as_object` returns the received data/object as is without casting it to a string like liquid normally does. It
can be used as a JSONPath replacement or to emit result of a Liquid filter chain as an array.

`catch` and `throw` needs to be used to break out of Liquid render chain. Liquid aggregates the output of every
expression an array and [joins](https://github.com/Shopify/liquid/blob/v3.0.6/lib/liquid/block.rb#L147) it together that
join makes it impossible to get anything else than a string out of a Liquid template.
Dominik Sander 8 жил өмнө
parent
commit
d2cbd04ac8

+ 11 - 6
app/concerns/liquid_droppable.rb

@@ -18,6 +18,11 @@ module LiquidDroppable
         yield [name, __send__(name)]
       }
     end
+
+    def as_json
+      return {} unless defined?(self.class::METHODS)
+      Hash[self.class::METHODS.map { |m| [m, send(m).as_json]}]
+    end
   end
 
   included do
@@ -33,12 +38,10 @@ module LiquidDroppable
     self.class::Drop.new(self)
   end
 
-  class MatchDataDrop < Liquid::Drop
-    def initialize(object)
-      @object = object
-    end
+  class MatchDataDrop < Drop
+    METHODS = %w[pre_match post_match names size]
 
-    %w[pre_match post_match names size].each { |attr|
+    METHODS.each { |attr|
       define_method(attr) {
         @object.__send__(attr)
       }
@@ -64,7 +67,9 @@ module LiquidDroppable
   require 'uri'
 
   class URIDrop < Drop
-    URI::Generic::COMPONENT.each { |attr|
+    METHODS = URI::Generic::COMPONENT
+
+    METHODS.each { |attr|
       define_method(attr) {
         @object.__send__(attr)
       }

+ 22 - 1
app/concerns/liquid_interpolatable.rb

@@ -92,7 +92,9 @@ module LiquidInterpolatable
 
   def interpolate_string(string, self_object = nil)
     interpolate_with(self_object) do
-      Liquid::Template.parse(string).render!(interpolation_context)
+      catch :as_object do
+        Liquid::Template.parse(string).render!(interpolation_context)
+      end
     end
   end
 
@@ -225,6 +227,25 @@ module LiquidInterpolatable
       JSON.dump(input)
     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
+
     private
 
     def logger

+ 4 - 2
app/models/agent.rb

@@ -443,7 +443,7 @@ class AgentDrop
     @object.short_type
   end
 
-  [
+  METHODS = [
     :name,
     :type,
     :options,
@@ -456,7 +456,9 @@ class AgentDrop
     :disabled,
     :keep_events_for,
     :propagate_immediately,
-  ].each { |attr|
+  ]
+
+  METHODS.each { |attr|
     define_method(attr) {
       @object.__send__(attr)
     } unless method_defined?(attr)

+ 4 - 0
app/models/event.rb

@@ -119,4 +119,8 @@ class EventDrop
   def _location_
     @object.location
   end
+
+  def as_json
+    {location: _location_.as_json, agent: @object.agent.to_liquid.as_json, payload: @payload.as_json, created_at: created_at.as_json}
+  end
 end

+ 55 - 0
spec/concerns/liquid_interpolatable_spec.rb

@@ -264,4 +264,59 @@ describe LiquidInterpolatable::Filters do
       expect(agent.interpolated['cleaned']).to eq('FOObar ZOObar')
     end
   end
+
+  context 'as_object' do
+    let(:agent) { Agents::InterpolatableAgent.new(name: "test") }
+
+    it 'returns an array that was splitted in liquid tags' do
+      agent.interpolation_context['something'] = 'test,string,abc'
+      agent.options['array'] = "{{something | split: ',' | as_object}}"
+      expect(agent.interpolated['array']).to eq(['test', 'string', 'abc'])
+    end
+
+    it 'returns an object that was not modified in liquid' do
+      agent.interpolation_context['something'] = {'nested' => {'abc' => 'test'}}
+      agent.options['object'] = "{{something.nested | as_object}}"
+      expect(agent.interpolated['object']).to eq({"abc" => 'test'})
+    end
+
+    context 'as_json' do
+      def ensure_safety(obj)
+        JSON.parse(JSON.dump(obj))
+      end
+
+      it 'it converts "complex" objects' do
+        agent.interpolation_context['something'] = {'nested' => Service.new}
+        agent.options['object'] = "{{something | as_object}}"
+        expect(agent.interpolated['object']).to eq({'nested'=> ensure_safety(Service.new.as_json)})
+      end
+
+      it 'works with AgentDrops' do
+        agent.interpolation_context['something'] = agent
+        agent.options['object'] = "{{something | as_object}}"
+        expect(agent.interpolated['object']).to eq(ensure_safety(agent.to_liquid.as_json.stringify_keys))
+      end
+
+      it 'works with EventDrops' do
+        event = Event.new(payload: {some: 'payload'}, agent: agent, created_at: Time.now)
+        agent.interpolation_context['something'] = event
+        agent.options['object'] = "{{something | as_object}}"
+        expect(agent.interpolated['object']).to eq(ensure_safety(event.to_liquid.as_json.stringify_keys))
+      end
+
+      it 'works with MatchDataDrops' do
+        match = "test string".match(/\A(?<word>\w+)\s(.+?)\z/)
+        agent.interpolation_context['something'] = match
+        agent.options['object'] = "{{something | as_object}}"
+        expect(agent.interpolated['object']).to eq(ensure_safety(match.to_liquid.as_json.stringify_keys))
+      end
+
+      it 'works with URIDrops' do
+        uri = URI.parse("https://google.com?q=test")
+        agent.interpolation_context['something'] = uri
+        agent.options['object'] = "{{something | as_object}}"
+        expect(agent.interpolated['object']).to eq(ensure_safety(uri.to_liquid.as_json.stringify_keys))
+      end
+    end
+  end
 end