tataru 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e42d1ffd98cb2d158b53f58b572cba30e0e08cb9da10f65386260bf24156b069
4
+ data.tar.gz: 2078749626908e57c5bef6759dbba0ff2e244638f4811c36d1cfc97dc841a688
5
+ SHA512:
6
+ metadata.gz: c9eefcedd70c5328abbae21d9e286d751e819f28b7fb85c64468aa3f978ddabb29f3f40cecf4bf557e9eb0da4ee6e78a8cf402b4e49ed9e145586ac3df52cbee
7
+ data.tar.gz: c5f88a391a8f67f6ab30afd095fbf438a8ae76dcc08e7febbaba563d733f5209da60bbb6c23e3d9f343e0256c04c329f689f1b0dde156083c0d9c3e348f33553
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/lib/tataru.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/inflector'
4
+ require 'bunny/tsort'
5
+
6
+ require 'tataru/version'
7
+ require 'tataru/resource'
8
+ require 'tataru/state'
9
+ require 'tataru/instruction'
10
+ require 'tataru/execution_step'
11
+ require 'tataru/planner'
12
+ require 'tataru/default_resource_finder'
13
+ require 'tataru/requirements_dsl'
14
+ require 'tataru/resource_dsl'
15
+ require 'tataru/requirements'
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tataru
4
+ # finds resource classes
5
+ class DefaultResourceFinder
6
+ def resource_named(name)
7
+ Kernel.const_get("Tataru::Resources::#{name.to_s.camelize}Resource")
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tataru
4
+ # An execution step
5
+ class ExecutionStep
6
+ def initialize(state, instruction)
7
+ @state = state
8
+ @instruction = instruction
9
+ @instances = {}
10
+ end
11
+
12
+ def id
13
+ @instruction.id
14
+ end
15
+
16
+ def rf
17
+ @instruction.requirements.resource_finder
18
+ end
19
+
20
+ def type_of_id
21
+ @instruction.requirements.type(id)
22
+ end
23
+
24
+ def class_of_id
25
+ rf.resource_named(type_of_id)
26
+ end
27
+
28
+ def instance_of_id
29
+ @instances[id] ||= class_of_id.new
30
+ end
31
+
32
+ def execute
33
+ return execute_begin! if @instruction.action.to_s.start_with?('begin_')
34
+
35
+ execute_wait!
36
+ end
37
+
38
+ def overall_action
39
+ @instruction.action.to_s.split('_')[1].to_sym
40
+ end
41
+
42
+ def execute_begin!
43
+ instance_of_id.send(@instruction.action, @state)
44
+ [send(:"begin_#{overall_action}"), true]
45
+ end
46
+
47
+ def begin_create
48
+ new_state = @state.clone
49
+ replacer = true
50
+ replacer = false if new_state[id].nil?
51
+
52
+ @instruction.state.each do |name, value|
53
+ new_state.putstate(id, name, value, replacer: replacer)
54
+ end
55
+ new_state.waiting_on(id, overall_action)
56
+ new_state
57
+ end
58
+
59
+ def begin_delete
60
+ begin_update
61
+ end
62
+
63
+ def begin_update
64
+ new_state = @state.clone
65
+ new_state.waiting_on(id, overall_action)
66
+ new_state
67
+ end
68
+
69
+ def execute_wait!
70
+ new_state = @state.clone
71
+ success = instance_of_id.send(:"#{overall_action}_complete?", @state)
72
+ if success
73
+ new_state.no_longer_waiting(id)
74
+ new_state.replace(id) if overall_action == :delete
75
+ end
76
+ [new_state, success]
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tataru
4
+ # An instruction
5
+ class Instruction
6
+ attr_reader :action, :id, :state, :requirements
7
+
8
+ def initialize(action, id, state, requirements)
9
+ @action = action
10
+ @id = id
11
+ @state = state
12
+ @requirements = requirements
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tataru
4
+ module Rage
5
+ # not valid
6
+ class InvalidRequirement < StandardError; end
7
+ end
8
+
9
+
10
+ # A plan
11
+ class Planner
12
+ def initialize(current_state, requirement)
13
+ @current_state = current_state
14
+ @requirement = requirement
15
+ @actions = {}
16
+
17
+ raise Rage::InvalidRequirement unless requirement.valid?
18
+ end
19
+
20
+ def order
21
+ Bunny::Tsort.tsort(@requirement.dep_tree)
22
+ end
23
+
24
+ def delete_instruction_for(id, pref)
25
+ Instruction.new(:"#{pref}_delete", id, @current_state[id], @requirement)
26
+ end
27
+
28
+ def generate_delete_instruction_for(id, pref)
29
+ return if @current_state[id].nil?
30
+ return unless action(id) == :replace
31
+
32
+ delete_instruction_for(id, pref)
33
+ end
34
+
35
+ def generate_instruction_for(id, pref)
36
+ if @current_state[id].nil?
37
+ Instruction.new(:"#{pref}_create", id, end_state[id], @requirement)
38
+ elsif action(id) == :replace
39
+ Instruction.new(:"#{pref}_create", id, end_state[id], @requirement)
40
+ elsif action(id) == :update
41
+ Instruction.new(:"#{pref}_update", id, end_state[id], @requirement)
42
+ end
43
+ end
44
+
45
+ def generate_removal_instructions
46
+ remove_actions = []
47
+ @current_state.id_list.keys.each do |id|
48
+ next if @requirement.exist? id
49
+
50
+ remove_actions << delete_instruction_for(id, :begin)
51
+ end
52
+ remove_actions
53
+ end
54
+
55
+ def generate_delete_instructions
56
+ delete_actions = []
57
+ order.reverse.each do |step|
58
+ %i[begin wait].each do |substep|
59
+ step.each do |id|
60
+ delete_action = generate_delete_instruction_for(id, substep)
61
+ delete_actions << delete_action unless delete_action.nil?
62
+ end
63
+ end
64
+ end
65
+ delete_actions + generate_removal_instructions
66
+ end
67
+
68
+ def generate_instructions
69
+ instr = []
70
+ order.each do |step|
71
+ %i[begin wait].each do |substep|
72
+ step.each do |id|
73
+ instruction = generate_instruction_for(id, substep)
74
+ instr << instruction unless instruction.nil?
75
+ end
76
+ end
77
+ end
78
+ instr + generate_delete_instructions
79
+ end
80
+
81
+ def instructions
82
+ @instructions ||= generate_instructions
83
+ end
84
+
85
+ def action(id)
86
+ @actions[id] ||= @requirement.action(id, @current_state[id])
87
+ end
88
+
89
+ def end_state
90
+ @requirement.end_state
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tataru
4
+ # Requirements list
5
+ class Requirements
6
+ attr_reader :resource_finder, :errors
7
+
8
+ def initialize(resource_finder = DefaultResourceFinder.new, &block)
9
+ dsl = RequirementsDSL.new(resource_finder)
10
+ dsl.instance_exec(&block)
11
+ @errors = dsl.errors
12
+ @reqs = dsl.resource_list
13
+ @resource_finder = resource_finder
14
+ end
15
+
16
+ def dep_tree
17
+ @reqs.map { |k, v| [k, v[:dependencies]] }.to_h
18
+ end
19
+
20
+ def end_state
21
+ state = State.new
22
+ @reqs.each do |id, info|
23
+ info[:state].each do |state_name, state_value|
24
+ state.putstate(id, state_name, state_value)
25
+ end
26
+ end
27
+ state
28
+ end
29
+
30
+ def exist?(id)
31
+ @reqs.key? id
32
+ end
33
+
34
+ def type(id)
35
+ @reqs[id][:type]
36
+ end
37
+
38
+ def valid?
39
+ errors.length.zero?
40
+ end
41
+
42
+ def compare(id, current_state)
43
+ rclass = @resource_finder.resource_named(type(id))
44
+ resdef = rclass.new
45
+ changed = false
46
+ replace = false
47
+
48
+ current_state.each do |state_name, current_value|
49
+ next if current_value == @reqs[id][:state][state_name]
50
+
51
+ changed = true
52
+ replace ||= resdef.send(:"#{state_name}_change_action") == :replace
53
+ end
54
+
55
+ [changed, replace]
56
+ end
57
+
58
+ def action(id, current_state)
59
+ changed, replace = compare(id, current_state)
60
+
61
+ return :nothing unless changed
62
+ return :update unless replace
63
+
64
+ :replace
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tataru
4
+ # Requirements DSL
5
+ class RequirementsDSL
6
+ attr_reader :resource_list
7
+
8
+ def initialize(resource_finder)
9
+ @resource_finder = resource_finder
10
+ @resource_list = {}
11
+ end
12
+
13
+ def respond_to_missing?
14
+ true
15
+ end
16
+
17
+ def method_missing(type, name, &block)
18
+ rclass = @resource_finder.resource_named(type)
19
+ res = ResourceDSL.new(rclass.new)
20
+ res.instance_exec(&block) if block
21
+ @resource_list[name] = {
22
+ type: type, dependencies: res.dependencies,
23
+ state: res.fields, errors: res.errors
24
+ }
25
+ rescue NameError
26
+ super
27
+ end
28
+
29
+ def errors
30
+ @resource_list.flat_map { |_, v| v[:errors] }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tataru
4
+ # base resource class
5
+ class Resource
6
+ class << self
7
+ def state(state_name, change_behaviour)
8
+ define_method "#{state_name}_change_action" do
9
+ change_behaviour
10
+ end
11
+
12
+ define_method "_state_#{state_name}" do
13
+ state_name
14
+ end
15
+ end
16
+
17
+ def output(output_name)
18
+ define_method "_output_#{output_name}" do
19
+ output_name
20
+ end
21
+
22
+ define_method(output_name) {}
23
+ end
24
+ end
25
+
26
+ def states
27
+ list_props(:state)
28
+ end
29
+
30
+ def outputs
31
+ list_props(:output)
32
+ end
33
+
34
+ %i[
35
+ begin_create
36
+ begin_update
37
+ begin_delete
38
+ create_complete?
39
+ update_complete?
40
+ delete_complete?
41
+ ].each do |thing|
42
+ define_method(thing) { |_state| }
43
+ end
44
+
45
+ private
46
+
47
+ def list_props(prop_type)
48
+ methods.select { |x| x.to_s.start_with? "_#{prop_type}_" }
49
+ .map { |x| x.to_s.sub("_#{prop_type}_", '').to_sym }
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tataru
4
+ module DoLater
5
+ # delayed expression
6
+ class Expression
7
+ def requested_resources
8
+ raise 'Abstract class'
9
+ end
10
+ end
11
+
12
+ # placeholder for extern resource
13
+ class ExternResourcePlaceholder < Expression
14
+ def initialize(name)
15
+ @name = name
16
+ end
17
+
18
+ def respond_to_missing?
19
+ true
20
+ end
21
+
22
+ def method_missing(name, *_args)
23
+ super if name.nil?
24
+
25
+ MemberCallPlaceholder.new(self, name)
26
+ end
27
+
28
+ def requested_resources
29
+ [@name]
30
+ end
31
+ end
32
+
33
+ # placeholder for member call
34
+ class MemberCallPlaceholder < ExternResourcePlaceholder
35
+ def initialize(expr, member)
36
+ @expr = expr
37
+ @member = member
38
+ end
39
+
40
+ def requested_resources
41
+ @expr.requested_resources
42
+ end
43
+ end
44
+ end
45
+
46
+ # Resource DSL
47
+ class ResourceDSL
48
+ attr_reader :fields, :extern_refs
49
+
50
+ def initialize(resource_inst)
51
+ @resource_inst = resource_inst
52
+ @fields = {}
53
+ @extern_refs = {}
54
+ end
55
+
56
+ def respond_to_missing?
57
+ true
58
+ end
59
+
60
+ def method_missing(name, *args, &block)
61
+ if @resource_inst.respond_to?("#{name}_change_action")
62
+ @fields[name] = args[0]
63
+ elsif name.to_s.start_with?(/[a-z]/)
64
+ @extern_refs[name] ||= DoLater::ExternResourcePlaceholder.new(name)
65
+ else
66
+ super
67
+ end
68
+ end
69
+
70
+ def errors
71
+ (@resource_inst.states - @fields.keys).map do |x|
72
+ { missing_state: x }
73
+ end
74
+ end
75
+
76
+ def dependencies
77
+ deps = []
78
+ @fields.each do |_name, info|
79
+ next unless info.is_a? DoLater::Expression
80
+
81
+ deps += info.requested_resources
82
+ end
83
+ deps.map(&:to_s).uniq
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tataru
4
+ # The state of the environment
5
+ class State
6
+ def initialize(objstate = {})
7
+ @current_ids = objstate[:current_ids] || {}
8
+ @replacer_ids = objstate[:replacer_ids] || {}
9
+ @waiting_ids = objstate[:waiting_ids] || {}
10
+ end
11
+
12
+ def putstate(id, state, value, replacer: false)
13
+ ids = id_list(replacer)
14
+ ids[id] = {} unless ids.key? id
15
+ ids[id][state] = value
16
+ end
17
+
18
+ def getstate(id, state, replacer: false)
19
+ ids = id_list(replacer)
20
+ return unless ids.key? id
21
+
22
+ ids[id][state]
23
+ end
24
+
25
+ def id_list(replacer = false)
26
+ return @replacer_ids if replacer
27
+
28
+ @current_ids
29
+ end
30
+
31
+ def replace(id)
32
+ @current_ids[id] = @replacer_ids[id]
33
+ @replacer_ids.delete(id)
34
+ end
35
+
36
+ def delete_list
37
+ @replacer_ids.keys.select { |x| @current_ids.key? x }
38
+ end
39
+
40
+ def [](id)
41
+ @current_ids[id].clone
42
+ end
43
+
44
+ def waiting_on(id, what)
45
+ @waiting_ids[id] = what
46
+ end
47
+
48
+ def no_longer_waiting(id)
49
+ @waiting_ids.delete(id)
50
+ end
51
+
52
+ def waiting_list
53
+ @waiting_ids.clone
54
+ end
55
+
56
+ def to_h
57
+ {
58
+ current_ids: @current_ids,
59
+ replacer_ids: @replacer_ids,
60
+ waiting_ids: @waiting_ids
61
+ }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tataru
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tataru
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - David Siaw
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-12-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bunny-tsort
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.17'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.17'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ description: Sworn upon the name of the receptionist
84
+ email:
85
+ - dsiaw@degica.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - Rakefile
91
+ - lib/tataru.rb
92
+ - lib/tataru/default_resource_finder.rb
93
+ - lib/tataru/execution_step.rb
94
+ - lib/tataru/instruction.rb
95
+ - lib/tataru/planner.rb
96
+ - lib/tataru/requirements.rb
97
+ - lib/tataru/requirements_dsl.rb
98
+ - lib/tataru/resource.rb
99
+ - lib/tataru/resource_dsl.rb
100
+ - lib/tataru/state.rb
101
+ - lib/tataru/version.rb
102
+ homepage: https://github.com/davidsiaw/tataru
103
+ licenses:
104
+ - MIT
105
+ metadata:
106
+ allowed_push_host: https://rubygems.org
107
+ homepage_uri: https://github.com/davidsiaw/tataru
108
+ source_code_uri: https://github.com/davidsiaw/tataru
109
+ changelog_uri: https://github.com/davidsiaw/tataru
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubygems_version: 3.0.3
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: Task planner
129
+ test_files: []