Remove "locals" from Drop classes and introduce a real context class.

The Context class is capable of switching a self object and adding local
variables, and also hashable for the convenience of being used as a
cache key.

As for the thread safety, we give it up for now as we don't support it
or have concurrency tests yet anyway.

Akinori MUSHA 10 年 前
コミット
e2f2ff5da8
共有5 個のファイルを変更した108 個の追加71 個の削除を含む
  1. 7 39
      app/concerns/liquid_droppable.rb
  2. 78 12
      app/concerns/liquid_interpolatable.rb
  3. 6 5
      app/models/agents/event_formatting_agent.rb
  4. 4 9
      app/models/agents/website_agent.rb
  5. 13 6
      app/models/event.rb

+ 7 - 39
app/concerns/liquid_droppable.rb

@@ -1,48 +1,16 @@
1
+# Include this mix-in to make a class droppable to Liquid, and adjust
2
+# its behavior in Liquid by implementing its dedicated Drop class
3
+# named with a "Drop" suffix.
1 4
 module LiquidDroppable
2 5
   extend ActiveSupport::Concern
3 6
 
4
-  # In subclasses of this base class, "locals" take precedence over
5
-  # methods.
6 7
   class Drop < Liquid::Drop
7
-    class << self
8
-      def inherited(subclass)
9
-        class << subclass
10
-          attr_reader :drop_methods
11
-
12
-          # Make all public methods private so that #before_method
13
-          # catches everything.
14
-          def drop_methods!
15
-            return if @drop_methods
16
-
17
-            @drop_methods = Set.new
18
-
19
-            (public_instance_methods - Drop.public_instance_methods).each { |name|
20
-              @drop_methods << name.to_s
21
-              private name
22
-            }
23
-          end
24
-        end
25
-      end
26
-    end
27
-
28
-    def initialize(object, locals = nil)
29
-      self.class.drop_methods!
30
-
8
+    def initialize(object)
31 9
       @object = object
32
-      @locals = locals || {}
33
-    end
34
-
35
-    def before_method(name)
36
-      if @locals.include?(name)
37
-        @locals[name]
38
-      elsif self.class.drop_methods.include?(name)
39
-        __send__(name)
40
-      end
41 10
     end
42 11
 
43 12
     def each
44
-      return to_enum(__method__) unless block_given?
45
-      self.class.drop_methods.each { |name|
13
+      (public_instance_methods - Drop.public_instance_methods).each { |name|
46 14
         yield [name, __send__(name)]
47 15
       }
48 16
     end
@@ -52,7 +20,7 @@ module LiquidDroppable
52 20
     const_set :Drop, Kernel.const_set("#{name}Drop", Class.new(Drop))
53 21
   end
54 22
 
55
-  def to_liquid(*args)
56
-    self.class::Drop.new(self, *args)
23
+  def to_liquid
24
+    self.class::Drop.new(self)
57 25
   end
58 26
 end

+ 78 - 12
app/concerns/liquid_interpolatable.rb

@@ -18,28 +18,94 @@ module LiquidInterpolatable
18 18
     false
19 19
   end
20 20
 
21
-  def interpolate_options(options, event = {})
22
-    case options
21
+  # Return the current interpolation context.  Use this in your Agent
22
+  # class to manipulate interpolation context for user.
23
+  #
24
+  # For example, to provide local variables:
25
+  #
26
+  #     # Create a new scope to define variables in:
27
+  #     interpolation_context.stack {
28
+  #       interpolation_context['_something_'] = 42
29
+  #       # And user can say "{{_something_}}" in their options.
30
+  #       value = interpolated['some_key']
31
+  #     }
32
+  #
33
+  def interpolation_context
34
+    @interpolation_context ||= Context.new(self)
35
+  end
36
+
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
+
65
+  def interpolate_options(options, self_object = nil)
66
+    interpolate_with(self_object) do
67
+      case options
23 68
       when String
24
-        interpolate_string(options, event)
69
+        interpolate_string(options)
25 70
       when ActiveSupport::HashWithIndifferentAccess, Hash
26
-        options.inject(ActiveSupport::HashWithIndifferentAccess.new) { |memo, (key, value)| memo[key] = interpolate_options(value, event); memo }
71
+        options.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) { |(key, value), memo|
72
+          memo[key] = interpolate_options(value)
73
+        }
27 74
       when Array
28
-        options.map { |value| interpolate_options(value, event) }
75
+        options.map { |value| interpolate_options(value) }
29 76
       else
30 77
         options
78
+      end
79
+    end
80
+  end
81
+
82
+  def interpolated(self_object = nil)
83
+    interpolate_with(self_object) do
84
+      (@interpolated_cache ||= {})[[options, interpolation_context]] ||=
85
+        interpolate_options(options)
31 86
     end
32 87
   end
33 88
 
34
-  def interpolated(event = {})
35
-    key = [options, event]
36
-    @interpolated_cache ||= {}
37
-    @interpolated_cache[key] ||= interpolate_options(options, event)
38
-    @interpolated_cache[key]
89
+  def interpolate_string(string, self_object = nil)
90
+    interpolate_with(self_object) do
91
+      Liquid::Template.parse(string).render!(interpolation_context)
92
+    end
39 93
   end
40 94
 
41
-  def interpolate_string(string, event)
42
-    Liquid::Template.parse(string).render!(event.to_liquid, registers: {agent: self})
95
+  class Context < Liquid::Context
96
+    def initialize(agent)
97
+      super({}, {}, { agent: agent }, true)
98
+    end
99
+
100
+    def hash
101
+      [@environments, @scopes, @registers].hash
102
+    end
103
+
104
+    def eql?(other)
105
+      other.environments == @environments &&
106
+        other.scopes == @scopes &&
107
+        other.registers == @registers
108
+    end
43 109
   end
44 110
 
45 111
   require 'uri'

+ 6 - 5
app/models/agents/event_formatting_agent.rb

@@ -120,11 +120,12 @@ module Agents
120 120
 
121 121
     def receive(incoming_events)
122 122
       incoming_events.each do |event|
123
-        payload = perform_matching(event.payload)
124
-        opts = interpolated(event.to_liquid(payload))
125
-        formatted_event = opts['mode'].to_s == "merge" ? event.payload.dup : {}
126
-        formatted_event.merge! opts['instructions']
127
-        create_event :payload => formatted_event
123
+        interpolate_with(event) do
124
+          interpolation_context.merge(perform_matching(event.payload))
125
+          formatted_event = interpolated['mode'].to_s == "merge" ? event.payload.dup : {}
126
+          formatted_event.merge! interpolated['instructions']
127
+          create_event :payload => formatted_event
128
+        end
128 129
       end
129 130
     end
130 131
 

+ 4 - 9
app/models/agents/website_agent.rb

@@ -205,16 +205,11 @@ module Agents
205 205
 
206 206
     def receive(incoming_events)
207 207
       incoming_events.each do |event|
208
-        Thread.current[:current_event] = event
209
-        url_to_scrape = event.payload['url']
210
-        check_url(url_to_scrape) if url_to_scrape =~ /^https?:\/\//i
208
+        interpolate_with(event) do
209
+          url_to_scrape = event.payload['url']
210
+          check_url(url_to_scrape) if url_to_scrape =~ /^https?:\/\//i
211
+        end
211 212
       end
212
-    ensure
213
-      Thread.current[:current_event] = nil
214
-    end
215
-
216
-    def interpolated(event = Thread.current[:current_event])
217
-      super
218 213
     end
219 214
 
220 215
     private

+ 13 - 6
app/models/event.rb

@@ -48,21 +48,28 @@ class Event < ActiveRecord::Base
48 48
 end
49 49
 
50 50
 class EventDrop
51
-  def initialize(object, locals = nil)
52
-    locals = object.payload.merge(locals || {})
51
+  def initialize(object)
52
+    @payload = object.payload
53 53
     super
54 54
   end
55 55
 
56
+  def before_method(key)
57
+    @payload[key]
58
+  end
59
+
56 60
   def each(&block)
57
-    return to_enum(__method__) unless block
58
-    @locals.each(&block)
61
+    @payload.each(&block)
59 62
   end
60 63
 
61 64
   def agent
62
-    @object.agent
65
+    @payload.fetch(__method__) {
66
+      @object.agent
67
+    }
63 68
   end
64 69
 
65 70
   def created_at
66
-    @object.created_at
71
+    @payload.fetch(__method__) {
72
+      @object.created_at
73
+    }
67 74
   end
68 75
 end