daf 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,25 @@
1
+ require 'daf/action'
2
+ require 'twilio-ruby'
3
+
4
+ module DAF
5
+ # An action that sends an sms using twilio based on parameters
6
+ class SMSAction < Action
7
+ attr_option :to, String, :required
8
+ attr_option :message, String, :required
9
+ attr_option :from, String, :required
10
+ attr_option :sid, String, :required
11
+ attr_option :token, String, :required
12
+
13
+ attr_output :message_id, String
14
+
15
+ def client
16
+ @client ||= Twilio::REST::Client.new(@sid, @token)
17
+ end
18
+
19
+ def invoke
20
+ @message_id = client.account.messages.create(body: @message,
21
+ to: @to,
22
+ from: @from)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,55 @@
1
+ require 'thread'
2
+ Dir[File.dirname(__FILE__) + '/monitors/*'].each { |file| require file }
3
+ Dir[File.dirname(__FILE__) + '/actions/*'].each { |file| require file }
4
+
5
+ module DAF
6
+ # Represents a pair of Action and Monitor objects
7
+ # when requested, will begin watching the Monitor
8
+ # and when it triggers will invoke the action by
9
+ # default Command continues monitoring forever
10
+ # though subclasses may override this behavior
11
+ class Command
12
+ # Create a new command object from a data source
13
+ # @param datasource [CommandDataSource] The data source to use to initialize
14
+ # command object
15
+ def initialize(datasource)
16
+ @datasource = datasource
17
+ end
18
+
19
+ # Begins executing the command by starting the monitor specified in
20
+ # the data source - will return immediately
21
+ def execute
22
+ @thread = Thread.new do
23
+ if Thread.current != Thread.main
24
+ loop do
25
+ @datasource.monitor.on_trigger do
26
+ @datasource.action.activate(@datasource.action_options)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def cancel
34
+ @thread.kill
35
+ end
36
+ end
37
+
38
+ # Exception generated during loading or execution of command
39
+ class CommandException < Exception
40
+ end
41
+
42
+ # Data source to initialize command with
43
+ class CommandDataSource
44
+ # Overridden by subclasses - returns options that should be passed to action
45
+ def action_options
46
+ end
47
+
48
+ # Overridden by subclasses - returns action that should be used by command
49
+ def action
50
+ end
51
+
52
+ def monitor
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,145 @@
1
+ module DAF
2
+ # Module used for configurable objects internally
3
+ # adds the has_option class method that creates an option
4
+ # for use on Monitor and Action subclasses - it exposes
5
+ # the options that are present, and required types, so that
6
+ # parsers and UI can view them if required
7
+ module Configurable
8
+ # Processes given parameter into the defined options previously declared
9
+ # includes validation for types and any custom validators delcared
10
+ #
11
+ # @param [Hash<String,Object>] Hash of option name/value pairs, values
12
+ # must conform to validation rules for options or exception will be raised
13
+ def process_options(options)
14
+ options.each do |key, value|
15
+ key = key.to_s
16
+ fail OptionException, "No Option #{key}" unless self.class.options[key]
17
+ opt = send("#{key}")
18
+ opt.value = value
19
+ fail OptionException, "Bad value for option #{key}" unless opt.valid?
20
+ end
21
+ validate_required_options
22
+ end
23
+
24
+ def validate_required_options
25
+ self.class.send('required_options').each do |name|
26
+ opt = send("#{name}")
27
+ fail OptionException,
28
+ "Required option #{name} missing or invalid" unless opt.valid?
29
+ end
30
+ end
31
+
32
+ def self.included(base)
33
+ base.send(:extend, ClassMethods)
34
+ end
35
+
36
+ private :validate_required_options
37
+ protected :process_options
38
+
39
+ # Class methods used by configurable classes
40
+ module ClassMethods
41
+ def setup_options
42
+ class_variable_get('@@options')
43
+ class_variable_get('@@required_options')
44
+ rescue
45
+ class_variable_set('@@options', {})
46
+ class_variable_set('@@required_options', [])
47
+ end
48
+
49
+ def setup_option(name, type, required, verifier)
50
+ define_method("#{name}") do
51
+ instance_variable_set('@' + name,
52
+ Option.new(name, type, verifier)) unless
53
+ instance_variable_get('@' + name)
54
+ instance_variable_get('@' + name)
55
+ end
56
+
57
+ class_variable_get('@@required_options') << name if
58
+ required == :required
59
+ class_variable_get('@@options')[name] = type
60
+ end
61
+
62
+ def setup_outputs
63
+ class_variable_get('@@outputs')
64
+ rescue
65
+ class_variable_set('@@outputs', {})
66
+ end
67
+
68
+ def setup_output(name, type)
69
+ define_method("#{name}") do
70
+ instance_variable_get("@#{name}")
71
+ end
72
+ class_variable_get('@@outputs')[name] = type
73
+ end
74
+
75
+ # Notes that this class has the specified option
76
+ #
77
+ # @param [String, Symbol] name Name of this option
78
+ # @param [Class] type Type required for this option - will be verified
79
+ # @param [optional, :optional, :required] required Is this option
80
+ # required to be set, or merely optional
81
+ def attr_option(name, type, required = :optional, &verifier)
82
+ name = name.to_s
83
+ setup_options
84
+ setup_option(name, type, required, verifier)
85
+ end
86
+
87
+ def attr_output(name, type)
88
+ name = name.to_s
89
+ setup_outputs
90
+ setup_output(name, type)
91
+ end
92
+
93
+ # Returns required set of options
94
+ # @return [Array<Option>] Required options for this class
95
+ def required_options
96
+ class_variable_get('@@required_options')
97
+ end
98
+
99
+ # Returns valid oset of options
100
+ # @return [Hash<String,Class>] Available set of options, with expected
101
+ # class for each.
102
+ def options
103
+ class_variable_get('@@options')
104
+ end
105
+
106
+ # Returns set of outputs that are set
107
+ # @return [Hash<String,Class>]] Outputs that are set on trigger, with
108
+ # types of each as values
109
+ def outputs
110
+ class_variable_get('@@outputs')
111
+ end
112
+
113
+ protected :attr_option, :attr_output
114
+ private :setup_options, :setup_option, :setup_output, :setup_outputs
115
+ end
116
+ end
117
+
118
+ # Used to store options - includes the expected type
119
+ # the name, and the value. Also includes validation logic
120
+ # - the absence of validation logic in the value= operator is
121
+ # intentional, as there may be cases where you can set an invalid
122
+ # option value
123
+ class Option
124
+ attr_reader :name, :type
125
+ attr_accessor :value
126
+
127
+ def initialize(name, type, verifier = nil)
128
+ @type = type
129
+ @name = name
130
+ @verifier = if verifier
131
+ verifier
132
+ else
133
+ true
134
+ end
135
+ end
136
+
137
+ def valid?
138
+ !@value.nil? && @value.is_a?(@type) &&
139
+ (@verifier == true || @verifier.call(@value))
140
+ end
141
+ end
142
+
143
+ class OptionException < Exception
144
+ end
145
+ end
@@ -0,0 +1,53 @@
1
+ require 'yaml'
2
+ require 'daf/command'
3
+
4
+ module DAF
5
+ # A datasource that is parsed out of a YAML file
6
+ # does not permit any dynamic updates, but useful
7
+ # for a basic command parser
8
+ class YAMLDataSource < CommandDataSource
9
+ attr_reader :monitor, :action
10
+
11
+ # Accepts the path of the YAML file to be parsed into
12
+ # commands - will throw a CommandException should it have
13
+ # invalid parameters
14
+ #
15
+ # @param filePath [String] Path for YAML file
16
+ def initialize(file_path)
17
+ configuration = YAML.load_file(file_path)
18
+ @action_class, @monitor_class = action_monitor_classes(configuration)
19
+ @monitor = @monitor_class.new(configuration['Monitor']['Options'])
20
+ @action = @action_class.new
21
+ @action_options = configuration['Action']['Options']
22
+ end
23
+
24
+ def action_options
25
+ # Attempt resolution to outputs of monitor
26
+ return @action_options unless @monitor_class.outputs.length > 0
27
+ action_options = @action_options.clone
28
+ @monitor_class.outputs.each do |output, _type|
29
+ action_options.each do |option_key, option_value|
30
+ action_options[option_key] =
31
+ option_value.gsub("{{#{output}}}", @monitor.send(output).to_s)
32
+ end
33
+ end
34
+ action_options
35
+ end
36
+
37
+ def action_monitor_classes(configuration)
38
+ begin
39
+ action_class = get_class(configuration['Action']['Type'])
40
+ monitor_class = get_class(configuration['Monitor']['Type'])
41
+ rescue
42
+ raise CommandException, 'Invalid Action or Monitor type'
43
+ end
44
+ [action_class, monitor_class]
45
+ end
46
+
47
+ def get_class(class_name)
48
+ Object.const_get(class_name)
49
+ end
50
+
51
+ protected :get_class, :action_monitor_classes
52
+ end
53
+ end
@@ -0,0 +1,27 @@
1
+ require 'daf/configurable'
2
+
3
+ module DAF
4
+ # Stores information relating to things being monitored
5
+ # sub-classes define specific criteria for when a monitor
6
+ # should 'go off'. Has only one method, #on_trigger, that
7
+ # allows you to begin monitoring for an event
8
+ class Monitor
9
+ include Configurable
10
+
11
+ # Requires the set of options expected by this monitor
12
+ #
13
+ # @param options [Hash] The options in key/value format,
14
+ # the type of each option must match that expected or an
15
+ # exception will be raised
16
+ def initialize(options)
17
+ process_options(options)
18
+ end
19
+
20
+ # Begins monitoring for event, when event occurs will
21
+ # execute required block parameter
22
+ def on_trigger
23
+ block_until_triggered
24
+ yield
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,46 @@
1
+ require 'daf/monitor'
2
+
3
+ module DAF
4
+ # Monitor that watches a file's last update time, and triggers when it changes
5
+ # includes several return outputs that can be used as well
6
+ class FileUpdateMonitor < Monitor
7
+ attr_option :path, String, :required do |val|
8
+ File.exist? val
9
+ end
10
+
11
+ attr_option :frequency, Integer, :required do |val|
12
+ val > 1
13
+ end
14
+
15
+ # @return [Time] The last modified time of file that caused trigger
16
+ attr_output :time, Time
17
+
18
+ # @return [String] The contents of the tile that caused trigger
19
+ attr_output :contents, String
20
+
21
+ def initialize(options)
22
+ super
23
+ end
24
+
25
+ def block_until_triggered
26
+ initial_modified_time = File.mtime(@path.value)
27
+ loop do
28
+ sleep @frequency.value
29
+ modified_time = File.mtime(@path.value)
30
+ next unless modified_time > initial_modified_time
31
+ @time = modified_time
32
+ @contents = contents_of_file(@path.value)
33
+ break
34
+ end
35
+ end
36
+
37
+ def contents_of_file(path)
38
+ file = File.open(path)
39
+ contents = file.read
40
+ file.close
41
+ contents
42
+ end
43
+
44
+ private :contents_of_file
45
+ end
46
+ end
@@ -0,0 +1,4 @@
1
+ # DAF module version numbering
2
+ module DAF
3
+ VERSION = '0.3.0'
4
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ # Test action to verify functionality
4
+ class TestAction < DAF::Action
5
+ attr_accessor :success
6
+ alias_method :invoke, :success
7
+ attr_option :option, String
8
+ end
9
+
10
+ describe DAF::Action do
11
+ let(:test_action) { TestAction.new }
12
+ let(:options) { { 'option' => 'test' } }
13
+
14
+ it 'should be configurable' do
15
+ mixed_in = DAF::Action.ancestors.select { |o| o.class == Module }
16
+ expect(mixed_in).to include(DAF::Configurable)
17
+ end
18
+
19
+ it 'should have an activate method' do
20
+ expect(test_action).to respond_to(:activate)
21
+ end
22
+
23
+ it 'should return whatever invoke returns' do
24
+ test_action.success = 123
25
+ expect(test_action.activate(options)).to eq(123)
26
+ end
27
+
28
+ it 'should yield to a given block with invoke return value' do
29
+ test_action.success = 123
30
+ expect { |b| test_action.activate(options, &b) }
31
+ .to yield_with_args(123)
32
+ end
33
+
34
+ it 'should set option values' do
35
+ test_action.activate(options)
36
+ expect(test_action.option.value).to eq('test')
37
+ end
38
+ end
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe DAF::Command do
4
+ let(:datasource) do
5
+ datasource = double('DAF::CommandDataSource')
6
+ allow(datasource).to receive(:monitor).and_return(monitor)
7
+ allow(datasource).to receive(:action).and_return(action)
8
+ allow(datasource).to receive(:action_options).and_return({})
9
+ datasource
10
+ end
11
+ let(:monitor) { double('DAF::Monitor') }
12
+ let(:action) { double('DAF::Action') }
13
+
14
+ context 'when initialize is called' do
15
+ it 'should accept a datasource' do
16
+ expect { DAF::Command.new(datasource) }.to_not raise_error
17
+ end
18
+ end
19
+
20
+ context 'when execute is called' do
21
+ before(:each) do
22
+ allow(action).to receive(:activate)
23
+ allow(monitor).to receive(:on_trigger).and_yield
24
+ end
25
+
26
+ let(:ithread) do
27
+ ithread = double('Thread')
28
+ allow(ithread).to receive(:kill)
29
+ end
30
+
31
+ let(:thread) do
32
+ thread = class_double('Thread').as_stubbed_const(
33
+ transfer_nested_constants: true)
34
+ allow(thread).to receive(:new).and_yield.and_return(ithread)
35
+ allow(thread).to receive(:kill)
36
+ allow(thread).to receive(:current).and_return(ithread)
37
+ allow(thread).to receive(:main).and_return(ithread)
38
+ thread
39
+ end
40
+
41
+ let(:command) do
42
+ DAF::Command.new(datasource)
43
+ end
44
+
45
+ it 'should create a new thread' do
46
+ expect(thread).to receive(:new)
47
+ command.execute
48
+ end
49
+
50
+ it 'should terminate the thread when cancel called' do
51
+ thread
52
+ expect(ithread).to receive(:kill)
53
+ command.execute
54
+ command.cancel
55
+ end
56
+
57
+ it 'should trigger on data source monitor' do
58
+ expect(monitor).to receive(:on_trigger).and_yield
59
+ command.execute
60
+ sleep(1)
61
+ command.cancel
62
+ end
63
+
64
+ it 'should active data source action on trigger' do
65
+ expect(action).to receive(:activate) # .with({})
66
+ command.execute
67
+ sleep(1)
68
+ command.cancel
69
+ end
70
+
71
+ it 'should loop when action is complete' do
72
+ expect(action).to receive(:activate).at_least(2).times
73
+ command.execute
74
+ sleep(1)
75
+ command.cancel
76
+ end
77
+ end
78
+ end