├── shard.lock ├── shard.yml ├── spec ├── data │ ├── test1.txt │ ├── test2.txt │ └── test3.txt └── lupin_spec.cr └── src ├── lupin.cr ├── lupin ├── command.cr ├── exception.cr ├── input_file.cr ├── pipe.cr ├── plugin.cr ├── plugins │ └── hello_world.cr └── task.cr └── runners └── task_runner.cr /shard.lock: -------------------------------------------------------------------------------- 1 | version: 1.0 2 | shards: 3 | bargs: 4 | github: crystalrealm/bargs 5 | version: 1.0.0 6 | 7 | epilog: 8 | github: crystalrealm/epilog 9 | version: 1.1.1 10 | 11 | stringpad: 12 | github: crystalrealm/stringpad 13 | commit: 27ed1fde22c2ae7d093beda95ed5be9620721183 14 | 15 | watcher: 16 | github: faustinoaq/watcher 17 | version: 0.3.0 18 | 19 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: lupin 2 | version: 1.0.1 3 | 4 | authors: 5 | - Mark Molnar 6 | - Milan Szekely 7 | 8 | targets: 9 | lupin: 10 | main: src/lupin.cr 11 | 12 | dependencies: 13 | epilog: 14 | github: crystalrealm/epilog 15 | version: 1.1.1 16 | bargs: 17 | github: crystalrealm/bargs 18 | watcher: 19 | github: faustinoaq/watcher 20 | 21 | crystal: 0.26.0 22 | 23 | license: MIT 24 | -------------------------------------------------------------------------------- /spec/data/test1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lupincr/lupin/f4d21ed25fe08f11546cfe4f70753e4278f0c203/spec/data/test1.txt -------------------------------------------------------------------------------- /spec/data/test2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lupincr/lupin/f4d21ed25fe08f11546cfe4f70753e4278f0c203/spec/data/test2.txt -------------------------------------------------------------------------------- /spec/data/test3.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lupincr/lupin/f4d21ed25fe08f11546cfe4f70753e4278f0c203/spec/data/test3.txt -------------------------------------------------------------------------------- /spec/lupin_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/lupin" 3 | require "../src/lupin/plugins/*" 4 | 5 | describe Lupin do 6 | it "works with a simple named task" do 7 | task = Lupin.task("test") 8 | task.name.should eq "test" 9 | end 10 | 11 | it "works with debug mode" do 12 | Lupin.debug_mode true 13 | Lupin.get_debug_mode.should eq true 14 | end 15 | 16 | it "works with src" do 17 | task = Lupin.task("test").src("./spec/data/*.txt") 18 | task.pipe.value.as(Array).size.should eq 3 19 | end 20 | 21 | it "works with src and dist" do 22 | task = Lupin.task("test") 23 | .src("./spec/data/*.txt") 24 | .dist("./spec/data_out") 25 | 26 | task.dist.should eq true 27 | task.dist_path.should eq "./spec/data_out" 28 | end 29 | 30 | it "works with a single default task" do 31 | Lupin.task("default_task").command("touch default1.txt") 32 | Lupin.task("default_task_cleanup").command("rm default1.txt") 33 | 34 | Lupin.default("default_task") 35 | Lupin.run_default 36 | exists = File.exists?("default1.txt") 37 | Lupin.run("default_task_cleanup") 38 | exists.should eq true 39 | end 40 | 41 | it "works with multiple default tasks" do 42 | Lupin.task("default_task").command("touch default1.txt") 43 | Lupin.task("default_task2").command("touch default2.txt") 44 | Lupin.task("default_task_cleanup").command("rm default1.txt && rm default2.txt") 45 | 46 | Lupin.default(["default_task", "default_task2"]) 47 | 48 | Lupin.run_default 49 | 50 | exists = File.exists?("default1.txt") 51 | exists2 = File.exists?("default2.txt") 52 | 53 | Lupin.run("default_task_cleanup") 54 | exists.should eq true 55 | exists2.should eq true 56 | end 57 | 58 | it "works with a shell command" do 59 | Lupin.task("command_task") 60 | .command("touch ./spec/test.txt") 61 | Lupin.task("command_task_cleanup") 62 | .command("rm ./spec/test.txt") 63 | 64 | Lupin.run("command_task") 65 | 66 | exists = File.exists?("./spec/test.txt") 67 | 68 | Lupin.run("command_task_cleanup") 69 | exists.should eq true 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /src/lupin.cr: -------------------------------------------------------------------------------- 1 | require "./lupin/*" 2 | require "./lupin/plugins/*" 3 | require "epilog" 4 | 5 | module Lupin 6 | VERSION = "1.0.1@alpha" 7 | @@debug = false 8 | @@tasks = [] of Task 9 | @@logger = Epilog::Logger.new 10 | @@default_tasks : Array(String) = [] of String 11 | 12 | # Creates a new task 13 | def self.task(name : String) 14 | task = Task.new(name, @@debug) 15 | @@tasks.push(task.as(Lupin::Task)) 16 | 17 | self.debug("Task '#{name}' created.") 18 | 19 | task 20 | end 21 | 22 | # Overload for creating a task with a callback function that gets executed first 23 | def self.task(name : String, cb) 24 | task = self.task(name) 25 | fn = cb.call 26 | task.pipe = Pipe(typeof(fn)).new(fn) 27 | task 28 | end 29 | 30 | # Run a task 31 | def self.run(name : String) 32 | @@tasks.each do |task| 33 | if name == task.name 34 | task.run 35 | end 36 | end 37 | end 38 | 39 | # Override to run multiple tasks 40 | # To be used for default tasks. 41 | def self.run(tasks : Array) 42 | tasks.each do |task| 43 | self.run(task) 44 | end 45 | end 46 | 47 | # Adds a default task 48 | def self.default(subtask : String) 49 | @@default_tasks.push(subtask) 50 | end 51 | 52 | # Add multiple default tasks 53 | def self.default(subtasks : Array(String)) 54 | @@default_tasks = subtasks 55 | end 56 | 57 | # Runs the default task(s) 58 | def self.run_default 59 | self.run(@@default_tasks) 60 | end 61 | 62 | # turns global debug mode on 63 | def self.debug_mode(debug) 64 | @@debug = debug 65 | end 66 | 67 | # returns the state of the debug mode 68 | def self.get_debug_mode 69 | @@debug 70 | end 71 | 72 | # Debugging utility 73 | private def self.debug(message) 74 | @@logger.debug message if @@debug 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /src/lupin/command.cr: -------------------------------------------------------------------------------- 1 | module Lupin 2 | class Command 3 | getter name, args, full 4 | 5 | def initialize(@name : String, @args : Array(String), @full : String) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /src/lupin/exception.cr: -------------------------------------------------------------------------------- 1 | module Lupin 2 | class LupinException < Exception 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /src/lupin/input_file.cr: -------------------------------------------------------------------------------- 1 | module Lupin 2 | class InputFile 3 | @name : String 4 | @path : String 5 | @contents : String 6 | 7 | getter name, path, contents 8 | setter name, path, contents 9 | 10 | def initialize(@name, @path, @contents) 11 | end 12 | 13 | def write 14 | File.write("#{@path}#{@name}", @contents) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/lupin/pipe.cr: -------------------------------------------------------------------------------- 1 | module Lupin 2 | class Pipe(T) 3 | getter value 4 | 5 | def initialize(@value : T) 6 | end 7 | 8 | def pipe(plugin : Plugin) 9 | plugin.run(@value.as(T)) 10 | end 11 | 12 | def type 13 | T 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /src/lupin/plugin.cr: -------------------------------------------------------------------------------- 1 | module Lupin 2 | abstract class Plugin 3 | def run 4 | end 5 | 6 | def on(event_type : String) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/lupin/plugins/hello_world.cr: -------------------------------------------------------------------------------- 1 | # Simple test plugin that writes hello world to all given files. 2 | 3 | module Lupin::Plugins 4 | class HelloWorld < Lupin::Plugin 5 | # TODO 6 | def run(files) 7 | files.as(Array(Lupin::InputFile)).each do |file| 8 | file.contents = "Hello World." 9 | end 10 | 11 | files 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /src/lupin/task.cr: -------------------------------------------------------------------------------- 1 | require "watcher" 2 | 3 | module Lupin 4 | class Task 5 | getter name, pipe, watch, dist, watch_path, dist_path, watch_block 6 | setter pipe 7 | 8 | @pipe : Pipe(Bool) | Pipe(Array(InputFile)) | Pipe(String) 9 | 10 | def initialize(@name : String, params, @debug = false) 11 | @pipe_classes = [] of Plugin 12 | @pipe = Pipe(Bool).new(false) 13 | @commands = [] of Command 14 | @logger = Epilog::Logger.new 15 | @watch = false 16 | @dist = false 17 | @watch_path = "" 18 | @dist_path = "" 19 | 20 | @watch_block = false 21 | end 22 | 23 | def run 24 | @logger.start("Running Task '#{name}'..") 25 | 26 | @commands.each do |command| 27 | begin 28 | run_command(command.name, command.args) 29 | rescue 30 | raise LupinException.new("Command '#{command.full}' failed on task '#{@name}'.") 31 | end 32 | end 33 | 34 | if @watch 35 | @logger.log("Watching for changes in #{@watch_path}..") 36 | run_watch 37 | else 38 | run_pipe 39 | end 40 | 41 | @logger.success("Task '#{name}' finished successfully.") 42 | self 43 | end 44 | 45 | private def run_pipe 46 | previous_value = @pipe.value 47 | self.debug(previous_value) 48 | @pipe_classes.each do |instance| 49 | instance.on("pre_run") 50 | previous_value = instance.run(previous_value) 51 | self.debug(previous_value) 52 | instance.on("after_run") 53 | 54 | if previous_value.is_a?(Nil) 55 | raise LupinException.new("Pipe '#{instance.class.name}' failed for task '#{@name}'. Possible nil return?") 56 | end 57 | end 58 | 59 | run_dist if @dist 60 | end 61 | 62 | # Load files with the given mode, according to the given path 63 | def src(path, mode = "w") 64 | files = Dir.glob(path).map do |file_path| 65 | name = File.basename(file_path) 66 | path = File.dirname(file_path) + "/" 67 | contents = File.read(file_path) 68 | InputFile.new(name, path, contents) 69 | end 70 | 71 | @pipe = Pipe(Array(InputFile)).new files 72 | self 73 | end 74 | 75 | def dist(out_path) 76 | @dist = true 77 | @dist_path = out_path 78 | self 79 | end 80 | 81 | def run_dist 82 | @watch_block = true 83 | if @pipe.type != Array(InputFile) 84 | raise LupinException.new("dist may only be used on Array(Lupin::InputFile)") 85 | end 86 | 87 | @pipe.value.as(Array(InputFile)).each do |file| 88 | file.path = @dist_path 89 | Dir.mkdir_p(@dist_path) if !Dir.exists?(@dist_path) 90 | 91 | file.write 92 | end 93 | 94 | # 1.5 is the sweet spot. 95 | # This is implemented so the watcher doesn't pick up the changes created by .dist 96 | delay (1.5) { @watch_block = false } 97 | self 98 | end 99 | 100 | def watch(dir) 101 | @watch_path = dir 102 | src(@watch_path) 103 | @watch = true 104 | self 105 | end 106 | 107 | def run_watch 108 | watch @watch_path do |event| 109 | event.on_change do |files| 110 | if !@watch_block 111 | run_pipe 112 | @logger.success("Watch pipeline executed successfully.") 113 | end 114 | end 115 | end 116 | end 117 | 118 | def pipe(plugin : Plugin) 119 | @pipe_classes.push(plugin) 120 | self 121 | end 122 | 123 | # Process and execute raw shell commands 124 | def command(command) 125 | split_commands = command.split("&&") 126 | 127 | split_commands.each do |text_command| 128 | text_command = text_command.chomp 129 | command_args = command.split(" ") 130 | command_name = command_args.delete_at(0) 131 | @commands.push(Command.new(command_name, command_args, text_command)) 132 | end 133 | 134 | self 135 | end 136 | 137 | private def run_command(name, args) 138 | status = Process.run(name, args: args) 139 | status.exit_code 140 | end 141 | 142 | private def debug(value) 143 | @logger.log "Previous value of pipeline: #{value.to_s}" if @debug 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /src/runners/task_runner.cr: -------------------------------------------------------------------------------- 1 | require "../lupin" 2 | require "../../../../lupinfile.cr" 3 | if ARGV.size > 0 4 | Lupin.run(ARGV) 5 | else 6 | Lupin.run_default 7 | end 8 | --------------------------------------------------------------------------------