daf 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +34 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +71 -0
- data/LICENSE.txt +21 -0
- data/README.md +37 -0
- data/Rakefile +23 -0
- data/bin/dad +5 -0
- data/daf.gemspec +35 -0
- data/examples/config/SendEmail.yaml +16 -0
- data/lib/daf.rb +50 -0
- data/lib/daf/action.rb +25 -0
- data/lib/daf/actions/email_action.rb +34 -0
- data/lib/daf/actions/pushbullet_action.rb +26 -0
- data/lib/daf/actions/shell_action.rb +21 -0
- data/lib/daf/actions/sms_action.rb +25 -0
- data/lib/daf/command.rb +55 -0
- data/lib/daf/configurable.rb +145 -0
- data/lib/daf/datasources/yaml_data_source.rb +53 -0
- data/lib/daf/monitor.rb +27 -0
- data/lib/daf/monitors/file_update_monitor.rb +46 -0
- data/lib/daf/version.rb +4 -0
- data/spec/action_spec.rb +38 -0
- data/spec/command_spec.rb +78 -0
- data/spec/configurable_spec.rb +62 -0
- data/spec/daf_spec.rb +93 -0
- data/spec/email_action_spec.rb +61 -0
- data/spec/file_update_monitor_spec.rb +85 -0
- data/spec/monitor_spec.rb +41 -0
- data/spec/shell_action_spec.rb +55 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/yaml_data_source_spec.rb +117 -0
- metadata +188 -0
@@ -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
|
data/lib/daf/command.rb
ADDED
@@ -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
|
data/lib/daf/monitor.rb
ADDED
@@ -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
|
data/lib/daf/version.rb
ADDED
data/spec/action_spec.rb
ADDED
@@ -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
|