java_script_agent.rb 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. require 'date'
  2. require 'cgi'
  3. module Agents
  4. class JavaScriptAgent < Agent
  5. include FormConfigurable
  6. can_dry_run!
  7. default_schedule "never"
  8. description <<-MD
  9. This Agent allows you to write code in JavaScript that can create and receive events. If other Agents aren't meeting your needs, try this one!
  10. You can put code in the `code` option, or put your code in a Credential and reference it from `code` with `credential:<name>` (recommended).
  11. You can implement `Agent.check` and `Agent.receive` as you see fit. The following methods will be available on Agent in the JavaScript environment:
  12. * `this.createEvent(payload)`
  13. * `this.incomingEvents()` (the returned event objects will each have a `payload` property)
  14. * `this.memory()`
  15. * `this.memory(key)`
  16. * `this.memory(keyToSet, valueToSet)`
  17. * `this.options()`
  18. * `this.options(key)`
  19. * `this.log(message)`
  20. * `this.error(message)`
  21. MD
  22. form_configurable :language, type: :array, values: %w[JavaScript CoffeeScript]
  23. form_configurable :code, type: :text, ace: true
  24. form_configurable :expected_receive_period_in_days
  25. form_configurable :expected_update_period_in_days
  26. def validate_options
  27. cred_name = credential_referenced_by_code
  28. if cred_name
  29. errors.add(:base, "The credential '#{cred_name}' referenced by code cannot be found") unless credential(cred_name).present?
  30. else
  31. errors.add(:base, "The 'code' option is required") unless options['code'].present?
  32. end
  33. if interpolated['language'].present? && !interpolated['language'].downcase.in?(%w[javascript coffeescript])
  34. errors.add(:base, "The 'language' must be JavaScript or CoffeeScript")
  35. end
  36. end
  37. def working?
  38. return false if recent_error_logs?
  39. if interpolated['expected_update_period_in_days'].present?
  40. return false unless event_created_within?(interpolated['expected_update_period_in_days'])
  41. end
  42. if interpolated['expected_receive_period_in_days'].present?
  43. return false unless last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago
  44. end
  45. true
  46. end
  47. def check
  48. log_errors do
  49. execute_js("check")
  50. end
  51. end
  52. def receive(incoming_events)
  53. log_errors do
  54. execute_js("receive", incoming_events)
  55. end
  56. end
  57. def default_options
  58. js_code = <<-JS
  59. Agent.check = function() {
  60. if (this.options('make_event')) {
  61. this.createEvent({ 'message': 'I made an event!' });
  62. var callCount = this.memory('callCount') || 0;
  63. this.memory('callCount', callCount + 1);
  64. }
  65. };
  66. Agent.receive = function() {
  67. var events = this.incomingEvents();
  68. for(var i = 0; i < events.length; i++) {
  69. this.createEvent({ 'message': 'I got an event!', 'event_was': events[i].payload });
  70. }
  71. }
  72. JS
  73. {
  74. 'code' => Utils.unindent(js_code),
  75. 'language' => 'JavaScript',
  76. 'expected_receive_period_in_days' => '2',
  77. 'expected_update_period_in_days' => '2'
  78. }
  79. end
  80. private
  81. def execute_js(js_function, incoming_events = [])
  82. js_function = js_function == "check" ? "check" : "receive"
  83. context = V8::Context.new
  84. context.eval(setup_javascript)
  85. context["doCreateEvent"] = lambda { |a, y| create_event(payload: clean_nans(JSON.parse(y))).payload.to_json }
  86. context["getIncomingEvents"] = lambda { |a| incoming_events.to_json }
  87. context["getOptions"] = lambda { |a, x| interpolated.to_json }
  88. context["doLog"] = lambda { |a, x| log x }
  89. context["doError"] = lambda { |a, x| error x }
  90. context["getMemory"] = lambda do |a, x, y|
  91. if x && y
  92. memory[x] = clean_nans(y)
  93. else
  94. memory.to_json
  95. end
  96. end
  97. if (options['language'] || '').downcase == 'coffeescript'
  98. context.eval(CoffeeScript.compile code)
  99. else
  100. context.eval(code)
  101. end
  102. context.eval("Agent.#{js_function}();")
  103. end
  104. def code
  105. cred = credential_referenced_by_code
  106. if cred
  107. credential(cred) || 'Agent.check = function() { this.error("Unable to find credential"); };'
  108. else
  109. interpolated['code']
  110. end
  111. end
  112. def credential_referenced_by_code
  113. interpolated['code'] =~ /\Acredential:(.*)\Z/ && $1
  114. end
  115. def setup_javascript
  116. <<-JS
  117. function Agent() {};
  118. Agent.createEvent = function(opts) {
  119. return JSON.parse(doCreateEvent(JSON.stringify(opts)));
  120. }
  121. Agent.incomingEvents = function() {
  122. return JSON.parse(getIncomingEvents());
  123. }
  124. Agent.memory = function(key, value) {
  125. if (typeof(key) !== "undefined" && typeof(value) !== "undefined") {
  126. getMemory(key, value);
  127. } else if (typeof(key) !== "undefined") {
  128. return JSON.parse(getMemory())[key];
  129. } else {
  130. return JSON.parse(getMemory());
  131. }
  132. }
  133. Agent.options = function(key) {
  134. if (typeof(key) !== "undefined") {
  135. return JSON.parse(getOptions())[key];
  136. } else {
  137. return JSON.parse(getOptions());
  138. }
  139. }
  140. Agent.log = function(message) {
  141. doLog(message);
  142. }
  143. Agent.error = function(message) {
  144. doError(message);
  145. }
  146. Agent.check = function(){};
  147. Agent.receive = function(){};
  148. JS
  149. end
  150. def log_errors
  151. begin
  152. yield
  153. rescue V8::Error => e
  154. error "JavaScript error: #{e.message}"
  155. end
  156. end
  157. def clean_nans(input)
  158. if input.is_a?(Array)
  159. input.map {|v| clean_nans(v) }
  160. elsif input.is_a?(Hash)
  161. input.inject({}) { |m, (k, v)| m[k] = clean_nans(v); m }
  162. elsif input.is_a?(Float) && input.nan?
  163. 'NaN'
  164. else
  165. input
  166. end
  167. end
  168. end
  169. end