tataru 0.1.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 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: []