1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
class Op
def check
raise "#{self.class} doesn't have a (required) 'check' method"
end
def do
raise "#{self.class} doesn't have a (required) 'do' method"
end
def undo
raise "#{self.class} doesn't have a (required) 'undo' method"
end
end
def defop(name, args, &block)
name = name.to_sym
eval "class #{name} < Op; end" # Must use `eval' to access
cls = eval "#{name}" # the global namespace
cls.instance_eval do
args.map {|a| attr_reader a.to_sym }
end
cls.define_method :initialize do |**kwargs|
args.each {|arg| instance_variable_set :"@#{arg}", kwargs[arg.to_sym] }
end
cls.define_method :to_s do inspect end
cls.define_method :to_str do to_s end
cls.instance_eval &block
end
defop :Write, [:file, :contents] do
define_method :check do
if File.exist? @file
if (File.read @file) != @contents
raise "File `#{@file}' exists but has unexpected contents"
else true
end
end
end
define_method :do do
write @file, @contents
end
define_method :undo do
rm @file
end
end
defop :Deploy, [:ops, :save] do
define_method :initialize do |save: nil, &block|
@ops = []
@save = save
self.instance_eval &block
end
define_method :check do
@ops.all? {|op| op.check } and
(@save ? self.save.check : true)
end
define_method :do do
self.load.undo if @save && self.save.check
@ops.each {|op| op.check || op.do }
self.save.do if @save
end
define_method :undo do
@ops.reverse.each {|op| op.check && op.undo }
self.save.undo if @save
end
define_method :save do
(Write.new file: @save,
contents: self.to_yaml)
end
define_method :load do
YAML.load_file(
@save,
permitted_classes: ([Op] + Op.subclasses))
end
end
def deploy(...)
Deploy.new(...)
end
def defdeploy(name, &block)
Deploy.define_method name, &block
end
defdeploy :op do |cls, **args|
@ops.push cls.new(**args)
end
|