daf 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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