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