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.
- 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
|