freud 0.0.1
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 +7 -0
- data/.gitignore +18 -0
- data/Gemfile +3 -0
- data/Guardfile +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +33 -0
- data/Rakefile +13 -0
- data/bin/freud +5 -0
- data/freud.gemspec +27 -0
- data/lib/freud.rb +5 -0
- data/lib/freud/config.rb +227 -0
- data/lib/freud/launcher.rb +121 -0
- data/lib/freud/logging.rb +44 -0
- data/lib/freud/pidfile.rb +49 -0
- data/lib/freud/runner.rb +197 -0
- data/lib/freud/variables.rb +64 -0
- data/lib/freud/version.rb +3 -0
- data/spec/config_spec.rb +141 -0
- data/spec/fixtures/true.json +6 -0
- data/spec/launcher_spec.rb +114 -0
- data/spec/pidfile_spec.rb +41 -0
- data/spec/runner_spec.rb +133 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/variables_spec.rb +74 -0
- metadata +194 -0
| @@ -0,0 +1,121 @@ | |
| 1 | 
            +
            require "agrippa/mutable"
         | 
| 2 | 
            +
            require "freud/logging"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Freud
         | 
| 5 | 
            +
                class Launcher
         | 
| 6 | 
            +
                    include Logging
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                    include Agrippa::Mutable
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                    state_reader %w(name root pidfile logfile background create_pidfile
         | 
| 11 | 
            +
                        reset_env env commands sudo_user args)
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    state_accessor :process
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    def default_state
         | 
| 16 | 
            +
                        { process: Process }
         | 
| 17 | 
            +
                    end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    def run(command, args = [])
         | 
| 20 | 
            +
                        @args = args
         | 
| 21 | 
            +
                        case(command)
         | 
| 22 | 
            +
                            when "help" then show_help
         | 
| 23 | 
            +
                            when "start" then daemonize(fetch_executable(command, true))
         | 
| 24 | 
            +
                            else execute(fetch_executable(command))
         | 
| 25 | 
            +
                        end
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    private
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    def fetch_executable(command, background = false)
         | 
| 31 | 
            +
                        apply_sudo(background) do
         | 
| 32 | 
            +
                            commands.fetch(command) do
         | 
| 33 | 
            +
                                show_help(false)
         | 
| 34 | 
            +
                                logger.fatal("Unknown command: #{command}") 
         | 
| 35 | 
            +
                            end
         | 
| 36 | 
            +
                        end
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    def apply_sudo(background)
         | 
| 40 | 
            +
                        command = yield
         | 
| 41 | 
            +
                        return(command) unless (sudo_user.to_s != "")
         | 
| 42 | 
            +
                        bash = sprintf('bash -c "%s"', command.gsub(/"/, '\\"'))
         | 
| 43 | 
            +
                        sudo_env = env.map { |key, value| sprintf('%s="%s"', key,
         | 
| 44 | 
            +
                            value.gsub(/"/, '\\"')) }.join(" ")
         | 
| 45 | 
            +
                        maybe_background = background ? "-b" : ""
         | 
| 46 | 
            +
                        sudo_options = "-n #{maybe_background} -u #{sudo_user}"
         | 
| 47 | 
            +
                        "sudo #{sudo_options} #{sudo_env} -- #{bash}"
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    def show_help(terminate = true)
         | 
| 51 | 
            +
                        logger.info("Valid commands: #{commands.keys.join(", ")}")
         | 
| 52 | 
            +
                        exit(0) if terminate
         | 
| 53 | 
            +
                        self
         | 
| 54 | 
            +
                    end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    def execute(command, options = nil)
         | 
| 57 | 
            +
                        log_runtime_environment(command)
         | 
| 58 | 
            +
                        $PROGRAM_NAME = command
         | 
| 59 | 
            +
                        process.exec(env, command, options || spawn_default_options)
         | 
| 60 | 
            +
                        self
         | 
| 61 | 
            +
                    end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                    def daemonize(command)
         | 
| 64 | 
            +
                        return(self) if running?
         | 
| 65 | 
            +
                        options = spawn_default_options
         | 
| 66 | 
            +
                        options[:err] = [ logfile, "a" ] if logfile
         | 
| 67 | 
            +
                        create_logfile
         | 
| 68 | 
            +
                        if background
         | 
| 69 | 
            +
                            options.merge!(pgroup: true)
         | 
| 70 | 
            +
                            log_runtime_environment(command, options)
         | 
| 71 | 
            +
                            pid = process.spawn(env, command, options)
         | 
| 72 | 
            +
                            maybe_create_pidfile(pid)
         | 
| 73 | 
            +
                        else
         | 
| 74 | 
            +
                            $PROGRAM_NAME = command
         | 
| 75 | 
            +
                            maybe_create_pidfile(process.pid)
         | 
| 76 | 
            +
                            execute(command, options)
         | 
| 77 | 
            +
                        end
         | 
| 78 | 
            +
                    end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                    def log_runtime_environment(command, options = nil)
         | 
| 81 | 
            +
                        options ||= spawn_default_options
         | 
| 82 | 
            +
                        logger.debug("running #{command}")
         | 
| 83 | 
            +
                        logger.debug("env #{ENV.inspect}")
         | 
| 84 | 
            +
                        logger.debug("env #{env.inspect}")
         | 
| 85 | 
            +
                        logger.debug("spawn_default_options #{options.inspect}")
         | 
| 86 | 
            +
                        self
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    def create_logfile
         | 
| 90 | 
            +
                        return unless logfile
         | 
| 91 | 
            +
                        begin
         | 
| 92 | 
            +
                            file = File.open(logfile, "a")
         | 
| 93 | 
            +
                            file.close
         | 
| 94 | 
            +
                        rescue
         | 
| 95 | 
            +
                            logger.fatal("Unable to open logfile: #{logfile}")
         | 
| 96 | 
            +
                        end
         | 
| 97 | 
            +
                        self
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                    def spawn_default_options
         | 
| 101 | 
            +
                        output = {}
         | 
| 102 | 
            +
                        output[:unsetenv_others] = (reset_env == true)
         | 
| 103 | 
            +
                        output[:chdir] = root
         | 
| 104 | 
            +
                        output[:close_others] = true
         | 
| 105 | 
            +
                        output[:in] = "/dev/null"
         | 
| 106 | 
            +
                        output[:out] = :err
         | 
| 107 | 
            +
                        output
         | 
| 108 | 
            +
                    end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                    def maybe_create_pidfile(pid)
         | 
| 111 | 
            +
                        return(self) unless (create_pidfile == true)
         | 
| 112 | 
            +
                        pidfile.write(pid)
         | 
| 113 | 
            +
                        self
         | 
| 114 | 
            +
                    end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                    # FIXME  Kill stale pidfile?
         | 
| 117 | 
            +
                    def running?
         | 
| 118 | 
            +
                        pidfile.running?
         | 
| 119 | 
            +
                    end
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
            end
         | 
| @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            require "logger"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Freud
         | 
| 4 | 
            +
                class RunnerExit < StandardError
         | 
| 5 | 
            +
                    attr_reader :message, :value
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                    def initialize(message, value = 1)
         | 
| 8 | 
            +
                        @message, @value = message, value
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                class FreudLogger < Logger
         | 
| 13 | 
            +
                    def initialize(*args)
         | 
| 14 | 
            +
                        super
         | 
| 15 | 
            +
                        debug_on = ENV.has_key?("DEBUG")
         | 
| 16 | 
            +
                        self.level = debug_on ? Logger::DEBUG : Logger::INFO
         | 
| 17 | 
            +
                        self.formatter = proc { |s, t, p, m| "#{m.strip}\n" }
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    def fatal(message)
         | 
| 21 | 
            +
                        super
         | 
| 22 | 
            +
                        raise(RunnerExit.new(message, 1))
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                module Logging
         | 
| 27 | 
            +
                    def self.log_to(stream)
         | 
| 28 | 
            +
                        @logger = FreudLogger.new(stream)
         | 
| 29 | 
            +
                        self
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    def self.logger
         | 
| 33 | 
            +
                        @logger ||= FreudLogger.new($stderr)
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    def logger
         | 
| 37 | 
            +
                        Freud::Logging.logger
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    def exit(value = 0)
         | 
| 41 | 
            +
                        raise(RunnerExit.new(nil, value))
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
            end
         | 
| @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            require "freud/logging"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Freud
         | 
| 4 | 
            +
                class Pidfile
         | 
| 5 | 
            +
                    include Logging
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                    def initialize(path)
         | 
| 8 | 
            +
                        @path = path
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    def write(pid)
         | 
| 12 | 
            +
                        File.open(@path, "w") { |f| f.write(pid.to_s) }
         | 
| 13 | 
            +
                        self
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    def read
         | 
| 17 | 
            +
                        return unless @path
         | 
| 18 | 
            +
                        return unless (File.exists?(@path) and File.readable?(@path))
         | 
| 19 | 
            +
                        output = File.read(@path)
         | 
| 20 | 
            +
                        output ? output.to_i : nil
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    def to_s
         | 
| 24 | 
            +
                        @path
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    def ==(other)
         | 
| 28 | 
            +
                        File.expand_path(to_s) == File.expand_path(other.to_s)
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    def kill(signal)
         | 
| 32 | 
            +
                        pid = read
         | 
| 33 | 
            +
                        return(self) unless pid
         | 
| 34 | 
            +
                        Process.kill(signal, pid)
         | 
| 35 | 
            +
                        self
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    def running?
         | 
| 39 | 
            +
                        begin
         | 
| 40 | 
            +
                            kill(0)
         | 
| 41 | 
            +
                            true
         | 
| 42 | 
            +
                        rescue Errno::ESRCH
         | 
| 43 | 
            +
                            false
         | 
| 44 | 
            +
                        rescue Errno::EPERM
         | 
| 45 | 
            +
                            true
         | 
| 46 | 
            +
                        end
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
            end
         | 
    
        data/lib/freud/runner.rb
    ADDED
    
    | @@ -0,0 +1,197 @@ | |
| 1 | 
            +
            require "freud/version"
         | 
| 2 | 
            +
            require "freud/logging"
         | 
| 3 | 
            +
            require "freud/config"
         | 
| 4 | 
            +
            require "freud/launcher"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Freud
         | 
| 7 | 
            +
                class Runner
         | 
| 8 | 
            +
                    include Logging
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                    def self.run(args = ARGV)
         | 
| 11 | 
            +
                        begin
         | 
| 12 | 
            +
                            new.run(args)
         | 
| 13 | 
            +
                        rescue RunnerExit => exception
         | 
| 14 | 
            +
                            exit(exception.value)
         | 
| 15 | 
            +
                        end
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    def run(args)
         | 
| 19 | 
            +
                        command = extract_command(args)
         | 
| 20 | 
            +
                        case(command)
         | 
| 21 | 
            +
                        when "version" then run_version
         | 
| 22 | 
            +
                        when "generate", "g" then run_generate(args)
         | 
| 23 | 
            +
                        when "@check" then run_check(args)
         | 
| 24 | 
            +
                        when "@wait-up" then run_wait_up(args)
         | 
| 25 | 
            +
                        when "@wait-down" then run_wait_down(args)
         | 
| 26 | 
            +
                        when "@signal-term" then run_signal(args, "TERM")
         | 
| 27 | 
            +
                        when "@signal-kill" then run_signal(args, "KILL")
         | 
| 28 | 
            +
                        when "@signal-hup" then run_signal(args, "HUP")
         | 
| 29 | 
            +
                        when "@dump" then run_dump(args)
         | 
| 30 | 
            +
                        else Launcher.new(fetch_config(args).to_hash).run(command, args)
         | 
| 31 | 
            +
                        end
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    private
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    def fetch_config(args)
         | 
| 37 | 
            +
                        file = extract_file(args)
         | 
| 38 | 
            +
                        stage = extract_stage(args)
         | 
| 39 | 
            +
                        Config.new.load(file, stage)
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    def run_version
         | 
| 43 | 
            +
                        logger.info Freud::VERSION
         | 
| 44 | 
            +
                        exit(0)
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    def run_generate(args)
         | 
| 48 | 
            +
                        logger.fatal("Usage: freud new [file]") unless args.first
         | 
| 49 | 
            +
                        path = args.first.sub(/(\.json)?$/, ".json")
         | 
| 50 | 
            +
                        name = File.basename(path).sub(/(\.json)?$/, "")
         | 
| 51 | 
            +
                        logger.fatal("File exists: #{path}") if File.exists?(path)
         | 
| 52 | 
            +
                        scaffold = <<-END
         | 
| 53 | 
            +
                            { 
         | 
| 54 | 
            +
                                "name": "#{name}",
         | 
| 55 | 
            +
                                "root": "#{File.expand_path(Dir.pwd)}",
         | 
| 56 | 
            +
                                "background": false,
         | 
| 57 | 
            +
                                "create_pidfile": false,
         | 
| 58 | 
            +
                                "reset_env": false,
         | 
| 59 | 
            +
                                "pidfile": "tmp/#{name}.pid",
         | 
| 60 | 
            +
                                "logfile": "log/#{name}.log",
         | 
| 61 | 
            +
                                "vars": {},
         | 
| 62 | 
            +
                                "env": {},
         | 
| 63 | 
            +
                                "stages": {
         | 
| 64 | 
            +
                                    "development": {},
         | 
| 65 | 
            +
                                    "production": {}
         | 
| 66 | 
            +
                                },
         | 
| 67 | 
            +
                                "commands": {
         | 
| 68 | 
            +
                                    "start": "/bin/false",
         | 
| 69 | 
            +
                                    "stop": "%freud @signal-term; %freud @wait-down",
         | 
| 70 | 
            +
                                    "restart": "%freud stop && %freud start",
         | 
| 71 | 
            +
                                    "reload": "%freud @signal-hup; %freud @wait-up",
         | 
| 72 | 
            +
                                    "kill": "%freud @signal-kill; %freud @wait-down",
         | 
| 73 | 
            +
                                    "status": "%freud @check"
         | 
| 74 | 
            +
                                }
         | 
| 75 | 
            +
                            }
         | 
| 76 | 
            +
                        END
         | 
| 77 | 
            +
                        lines = scaffold.lines.map { |l| l.rstrip.sub(/^\s{16}/, "") }
         | 
| 78 | 
            +
                        File.open(path, "w") { |f| f.write(lines.join("\n")) }
         | 
| 79 | 
            +
                        exit(0)
         | 
| 80 | 
            +
                    end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                    def run_check(args)
         | 
| 83 | 
            +
                        config = fetch_config(args)
         | 
| 84 | 
            +
                        print_status(config)
         | 
| 85 | 
            +
                    end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                    def print_status(config)
         | 
| 88 | 
            +
                        pidfile = config.fetch("pidfile")
         | 
| 89 | 
            +
                        name = config.fetch("name")
         | 
| 90 | 
            +
                        if pidfile.running?
         | 
| 91 | 
            +
                            pid = pidfile.read
         | 
| 92 | 
            +
                            logger.info("#{name} up with PID #{pid}.")
         | 
| 93 | 
            +
                        else
         | 
| 94 | 
            +
                            logger.info("#{name} down.")
         | 
| 95 | 
            +
                        end
         | 
| 96 | 
            +
                        exit(0)
         | 
| 97 | 
            +
                    end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                    def run_wait_down(args)
         | 
| 100 | 
            +
                        timeout = (extract_option(args, "-t", "--timeout") || 30).to_i
         | 
| 101 | 
            +
                        config = fetch_config(args)
         | 
| 102 | 
            +
                        pidfile = config.fetch("pidfile")
         | 
| 103 | 
            +
                        name = config.fetch("name")
         | 
| 104 | 
            +
                        started_at = Time.now.to_i
         | 
| 105 | 
            +
                        logger.info("Waiting #{timeout} seconds for #{name} to stop.") \
         | 
| 106 | 
            +
                            if pidfile.running?
         | 
| 107 | 
            +
                        while(pidfile.running?)
         | 
| 108 | 
            +
                            sleep(0.25)
         | 
| 109 | 
            +
                            next if ((Time.now.to_i - started_at) < timeout)
         | 
| 110 | 
            +
                            logger.info("#{name} not down within #{timeout} seconds.")
         | 
| 111 | 
            +
                            exit(1)
         | 
| 112 | 
            +
                        end
         | 
| 113 | 
            +
                        print_status(config)
         | 
| 114 | 
            +
                    end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                    def run_wait_up(args)
         | 
| 117 | 
            +
                        timeout = (extract_option(args, "-t", "--timeout") || 30).to_i
         | 
| 118 | 
            +
                        config = fetch_config(args)
         | 
| 119 | 
            +
                        pidfile = config.fetch("pidfile")
         | 
| 120 | 
            +
                        name = config.fetch("name")
         | 
| 121 | 
            +
                        started_at = Time.now.to_i
         | 
| 122 | 
            +
                        while(not pidfile.running?)
         | 
| 123 | 
            +
                            sleep(0.25)
         | 
| 124 | 
            +
                            next if ((Time.now.to_i - started_at) < timeout)
         | 
| 125 | 
            +
                            logger.info("#{name} not up within #{timeout} seconds.")
         | 
| 126 | 
            +
                            exit(1)
         | 
| 127 | 
            +
                        end
         | 
| 128 | 
            +
                        print_status(config)
         | 
| 129 | 
            +
                    end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                    def run_signal(args, signal)
         | 
| 132 | 
            +
                        config = fetch_config(args)
         | 
| 133 | 
            +
                        pidfile = config.fetch("pidfile")
         | 
| 134 | 
            +
                        exit(1) unless pidfile.running?
         | 
| 135 | 
            +
                        pidfile.kill(signal)
         | 
| 136 | 
            +
                        exit(0)
         | 
| 137 | 
            +
                    end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                    def run_dump(args)
         | 
| 140 | 
            +
                        fetch_config(args).dump
         | 
| 141 | 
            +
                        exit(0)
         | 
| 142 | 
            +
                    end
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                    def extract_flag(args, *flags)
         | 
| 145 | 
            +
                        flags.inject(false) { |out, flag| args.delete(flag) ? true : out }
         | 
| 146 | 
            +
                    end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                    def extract_option(args, *options)
         | 
| 149 | 
            +
                        output_args, index, value = [], 0, nil
         | 
| 150 | 
            +
                        while(index < args.length)
         | 
| 151 | 
            +
                            head = args[index]
         | 
| 152 | 
            +
                            tail = args[index + 1]
         | 
| 153 | 
            +
                            if options.include?(head)
         | 
| 154 | 
            +
                                index += 2
         | 
| 155 | 
            +
                                value = tail
         | 
| 156 | 
            +
                            else
         | 
| 157 | 
            +
                                index += 1
         | 
| 158 | 
            +
                                output_args.push(head)
         | 
| 159 | 
            +
                            end
         | 
| 160 | 
            +
                        end
         | 
| 161 | 
            +
                        args.replace(output_args)
         | 
| 162 | 
            +
                        value
         | 
| 163 | 
            +
                    end
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                    def extract_command(args)
         | 
| 166 | 
            +
                        return(args.shift) unless args.empty?
         | 
| 167 | 
            +
                        usage
         | 
| 168 | 
            +
                    end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                    def extract_file(args)
         | 
| 171 | 
            +
                        service_path = ENV["FREUD_SERVICE_PATH"] || "services"
         | 
| 172 | 
            +
                        path = args.shift
         | 
| 173 | 
            +
                        filename = first_file_in(path, "#{service_path}/#{path}.json",
         | 
| 174 | 
            +
                            ENV["FREUD_CONFIG"])
         | 
| 175 | 
            +
                        usage unless filename
         | 
| 176 | 
            +
                        logger.fatal("Can't open: #{filename}") \
         | 
| 177 | 
            +
                            unless (File.file?(filename) and File.readable?(filename))
         | 
| 178 | 
            +
                        File.open(filename, "r")
         | 
| 179 | 
            +
                    end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                    def extract_stage(args)
         | 
| 182 | 
            +
                        args.shift || ENV["FREUD_STAGE"] || "development"
         | 
| 183 | 
            +
                    end
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                    def first_file_in(*paths)
         | 
| 186 | 
            +
                        paths.each do |path|
         | 
| 187 | 
            +
                            next if path.nil?
         | 
| 188 | 
            +
                            return(path) if File.exists?(path)
         | 
| 189 | 
            +
                        end
         | 
| 190 | 
            +
                        nil
         | 
| 191 | 
            +
                    end
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                    def usage
         | 
| 194 | 
            +
                        logger.fatal("Usage: freud [command] [file] <stage>")
         | 
| 195 | 
            +
                    end
         | 
| 196 | 
            +
                end
         | 
| 197 | 
            +
            end
         | 
| @@ -0,0 +1,64 @@ | |
| 1 | 
            +
            require "agrippa/mutable_hash"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Freud
         | 
| 4 | 
            +
                class Variables
         | 
| 5 | 
            +
                    VARIABLES = /(?<!\\)%((\w+)|{(\w+)}|{(\w+)\|(.*)})/i
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                    ESCAPED_SIGILS = /\\%/
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    UNDEFINED = Object.new
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    include Agrippa::MutableHash
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    def initialize(*args)
         | 
| 14 | 
            +
                        super
         | 
| 15 | 
            +
                        @stack = {}
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    def each_pair(&block)
         | 
| 19 | 
            +
                        @state.each_pair(&block)
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    def test(input)
         | 
| 23 | 
            +
                        (input =~ VARIABLES) ? true : false
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    def merge(updates)
         | 
| 27 | 
            +
                        chain(updates)
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    def fetch(key, default = UNDEFINED)
         | 
| 31 | 
            +
                        key = key.to_sym
         | 
| 32 | 
            +
                        return(@state.fetch(key, default)) unless (default == UNDEFINED)
         | 
| 33 | 
            +
                        @state.fetch(key) { raise(KeyError, "Unknown variable: #{key}") }
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    def apply(input)
         | 
| 37 | 
            +
                        return(nil) if input.nil?
         | 
| 38 | 
            +
                        interpolated = input.gsub(VARIABLES) do 
         | 
| 39 | 
            +
                            key = $~[2] || $~[3] || $~[4] 
         | 
| 40 | 
            +
                            default = $~[5] || UNDEFINED
         | 
| 41 | 
            +
                            push_stack(key, input)
         | 
| 42 | 
            +
                            output = apply(fetch(key, default).to_s)
         | 
| 43 | 
            +
                            pop_stack(key)
         | 
| 44 | 
            +
                            output
         | 
| 45 | 
            +
                        end
         | 
| 46 | 
            +
                        interpolated.gsub(ESCAPED_SIGILS, "%")
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    def push_stack(key, input)
         | 
| 50 | 
            +
                        if @stack[key]
         | 
| 51 | 
            +
                            message = "Infinite loop evaluating '%#{key}' in '#{input}'"
         | 
| 52 | 
            +
                            raise(RuntimeError, message)
         | 
| 53 | 
            +
                        else
         | 
| 54 | 
            +
                            @stack[key] = true
         | 
| 55 | 
            +
                            self
         | 
| 56 | 
            +
                        end
         | 
| 57 | 
            +
                    end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    def pop_stack(key)
         | 
| 60 | 
            +
                        @stack.delete(key)
         | 
| 61 | 
            +
                        self
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
            end
         |