class QB::Role

Contains info on a QB role.

Definitions

Definitions

Definitions

Definitions

Constants

BUILTIN_PATH

“Factory defaults” that {QB::Role::PATH} is initialized to, and what it gets reset to when {QB::Role.reset_path!} is called.

Read the {QB::Role::PATH} docs for details on how QB role paths work.

This value is deeply frozen, and you should not attempt to change it - mess with {QB::Role::PATH} instead.

@return [Array<String>]

PATH

Array of string paths to directories to search for roles or paths to `ansible.cfg` files to look for an extract role paths from.

Value is a duplicate of the frozen {QB::Role::BUILTIN_PATH}. You can reset to those values at any time via {QB::Role.reset_path!}.

For the moment at least you can just mutate this value like you would `$LOAD_PATH`:

QB::Role::PATH.unshift '~/where/some/roles/be'
QB::Role::PATH.unshift '~/my/ansible.cfg'

The paths are searched from first to last.

WARNING

Search is **deep** - don't point this at large directory trees and
expect any sort of reasonable performance (any directory that
contains `node_modules` is usually a terrible idea for instance).

@return [Array<String>]

Attributes

display_path[R]

@!attribute [r] display_path

the path to the role that we display. we only show the directory name for QB roles, and use {QB::Util.compact_path} to show `.` and `~` for paths relative to the current directory and home directory, respectively.

@return [Pathname]
meta_path[R]

@!attribute [r] meta_path

@return [String, nil]
  the path qb metadata was load from. `nil` if it's never been loaded
  or doesn't exist.
name[R]

@!attribute [r] name

@return [String]
  the role's ansible "name", which is it's directory name.
path[R]

@!attribute [r] path

@return [Pathname]
  location of the role directory.

Public Class Methods

available() click to toggle source

All {QB::Role} found in search path.

Does it's best to remove duplicates that end up being reached though multiple search paths (happens most in development).

@return [Array<QB::Role>]

# File lib/qb/role.rb, line 60
def self.available
  self.search_path.
    select {|search_dir|
      # make sure it's there (and a directory)
      search_dir.directory?
    }.
    map {|search_dir|
      ['', '.yml', '.yaml'].flat_map { |ext|
        Pathname.glob(search_dir.join '**', 'meta', "qb#{ ext }").
          map {|meta_path|
            [meta_path.dirname.dirname, search_dir: search_dir]
          }
      }
    }.
    flatten( 1 ).
    map { |args| QB::Role.new *args }.
    uniq
end
default_name_for(path) click to toggle source

Do our best to figure out a role name from a path (that might not exist).

We needs this when we're creating a role.

@param [String | Pathname] path

@return [String]

# File lib/qb/role/name.rb, line 61
def self.default_name_for path
  resolved_path = QB::Util.resolve path
  
  # Find the first directory in the search path that contains the path,
  # if any do.
  #
  # It *could* be in more than one in funky situations like overlapping
  # search paths or link silliness, but that doesn't matter - we consider
  # the first place we find it to be the relevant once, since the search
  # path is most-important-first.
  #
  search_dir = search_path.find { |pathname|
    resolved_path.fnmatch? ( pathname / '**' ).to_s
  }
  
  if search_dir.nil?
    # It's not in any of the search directories
    #
    # If it has 'roles' as a segment than use what's after the last occurrence
    # of that (unless there isn't anything).
    #
    segments = resolved_path.to_s.split File::SEPARATOR
    
    if index = segments.rindex( 'roles' )
      name_segs = segments[( index + 1 )..( -1 )]
      
      unless name_segs.empty?
        return File.join name_segs
      end
    end
    
    # Ok, that didn't work... just return the basename I guess...
    return File.basename resolved_path
    
  end
  
  # it's in the search path, return the relative path from the containing
  # search dir to the resolved path (string version of it).
  resolved_path.relative_path_from( search_dir ).to_s

end
get_include_path(role, option_meta, current_include_path) click to toggle source

Get the include path for an included role based on the option metadata that defines the include and the current include path.

@param role [Role]

the role to include.

@param option_meta [Hash]

the entry for the option in qb.yml

@param current_include_path [Array<string>]

@return [Array<string>]

include path for the included role.
# File lib/qb/role.rb, line 136
def self.get_include_path role, option_meta, current_include_path
  new_include_path = if option_meta.key? 'as'
    case option_meta['as']
    when nil, false
      # include it in with the parent role's options
      current_include_path
    when String
      current_include_path + [option_meta['as']]
    else
      raise QB::Role::MetadataError.new,
        "bad 'as' value: #{ option_meta.inspect }"
    end
  else
    current_include_path + [role.namespaceless]
  end
end
matches(input) click to toggle source

Get an array of {QB::Role} that match an input string.

This is the meat of whats needed to support {QB::Role.require}.

How it works is… tricky. Read the comments and play around with it is the bast I can offer right now.

@param [String] input

The input string to match against role paths and names. Primarily what
the user typed after `qb run` on the CLI.

@return [Array<QB::Role>]

# File lib/qb/role/matches.rb, line 37
def self.matches input
  # keep this here to we don't re-gen every loop
  available = self.available
  
  # first off, see if input matches any relative paths exactly
  available.each {|role|
    return [role] if role.display_path.to_s == input
  }
  
  # create an array of "separator" variations to try *exact* matching
  # against. in order of preference:
  #
  # 1.  exact input
  #     -   this means if you ended up with roles that actually *are*
  #         differentiated by '_/-' differences (which, IMHO, is a
  #         horrible fucking idea), you can get exactly what you ask for
  #         as a first priority
  # 2.  input with '-' changed to '_'
  #     -   prioritized because convention is to underscore-separate
  #         role names.
  # 3.  input with '_' changed to '-'
  #     -   really just for convenience's sake so you don't really have to
  #         remember what separator is used.
  #
  separator_variations = [
    input,
    input.gsub('-', '_'),
    input.gsub('_', '-'),
  ]
  
  # {QB::Role} method names to check against, from highest to lowest
  # precedence
  method_names = [
    # 1.  The path we display to the user. This comes first because typing
    #     in exactly what they see should always work.
    :display_name,
    
    # 2.  The role's full name (with namespace) as it is likely to be used
    #     in Ansible
    :name,
    
    # 3.  The part of the role after the namespace, which is far less
    #     specific, but nice short-hand if it's unique
    :namespaceless
  ]
  
  # 1.  Exact matches (allowing `-`/`_` substitution)
  #
  #     Highest precedence, guaranteeing that exact verbatim matches will
  #     always work (or that's the intent).
  #
  method_names.each { |method_name|
    separator_variations.each { |variation|
      matches = available.select { |role|
        role.public_send( method_name ) == variation
      }
      return matches unless matches.empty?
    }
  }
  
  # 2.  Prefix matches
  #
  #     Do any of {#display_path}, {#name} or {#namespaceless} or start with
  #     the input pattern?
  #
  method_names.each { |method_name|
    separator_variations.each { |variation|
      matches = available.select { |role|
        role.public_send( method_name ).start_with? variation
      }
      return matches unless matches.empty?
    }
  }
  
  # 3.  Word slice full matches
  #
  #     Split the {#display_name} and input first by `/` and `.` segments,
  #     then {String#downcase} each segments and split it into words (using
  #     {NRSER.words}).
  #
  #     Then see if the input appears in the role name.
  #
  #     We test only {#display_name} because it should always contain
  #     {#name} and {#namesaceless}, so it's pointless to test the other
  #     two after it).
  #
  
  word_parse = ->( string ) {
    string.split( /[\/\.]/ ).map { |seg| seg.downcase.words }
  }
  
  input_parse = word_parse.call input
  
  exact_word_slice_matches = available.select { |role|
    word_parse.call( role.display_name ).slice? input_parse
  }
  
  return exact_word_slice_matches unless exact_word_slice_matches.empty?
  
  # 4.  Word slice prefix matches
  #
  #     Same thing as (3), but do a prefix match instead of the entire
  #     words.
  #
  name_word_matches = available.select { |role|
    word_parse.call( role.display_name ).
      slice?( input_parse ) { |role_words, input_words|
        # Use a custom match block to implement prefix matching
        #
        # We want to match if each input word is the start of the
        # corresponding role name word
        #
        if role_words.length >= input_words.length
          input_words.each_with_index.all? { |input_word, index|
            role_words[index].start_with? input_word
          }
        else
          false
        end
      }
    QB::Util.words_start_with? role.display_path.to_s, input
  }
  return name_word_matches unless name_word_matches.empty?
  
  # nada
  []
end
namespace_for(name) click to toggle source
# File lib/qb/role/name.rb, line 107
def self.namespace_for name
  *namespace_segments, last = name.split File::Separator
  
  namespace_segments << last.split('.').first if last.include?('.')
   
  if namespace_segments.empty?
    nil
  else
    File.join *namespace_segments
  end
end
namespaceless_for(name) click to toggle source
# File lib/qb/role/name.rb, line 120
def self.namespaceless_for name
  File.basename( name ).split('.', 2).last
end
new(path, search_dir: nil) click to toggle source

Instantiate a Role.

@param [String|Pathname] path

location of the role directory

@param [nil, Pathname] search_dir

Directory in {QB::Role.search_path} that the role was found in.
Used to figure out it's name correctly when using directory-structure
namespacing.
# File lib/qb/role.rb, line 216
def initialize path, search_dir: nil
  @path = if path.is_a?(Pathname) then path else Pathname.new(path) end
  
  # check it...
  unless @path.exist?
    raise Errno::ENOENT.new @path.to_s
  end
  
  unless @path.directory?
    raise Errno::ENOTDIR.new @path.to_s
  end
  
  @display_path = self.class.to_display_path @path
  
  @meta_path = if (@path + 'meta' + 'qb').exist?
    @path + 'meta' + 'qb'
  elsif (@path + 'meta' + 'qb.yml').exist?
    @path + 'meta' + 'qb.yml'
  else
    raise Errno::ENOENT.new "#{ @path.join('meta').to_s }/[qb|qb.yml]"
  end
  
  
  if search_dir.nil?
    @name = @path.to_s.split(File::SEPARATOR).last
  else
    @name = @path.relative_path_from(search_dir).to_s
  end
end
require(input) click to toggle source

Find exactly one matching role for the input string or raise.

Where we look is determined by {QB::Role::PATH} via {QB::Role.search_path}.

@param [String] input

Input string term used to search (what we got off the CLI args).

@return [QB::Role]

The single matching role.

@raise [QB::Role::NoMatchesError]

If we didn't find any matches.

@raise [QB::Role::MultipleMatchesError]

If we matched more than one role.
# File lib/qb/role.rb, line 96
def self.require input
  as_pathname = Pathname.new(input)
    
  # allow a path to a role dir
  if role_dir? as_pathname
    return QB::Role.new as_pathname
  end
  
  matches = self.matches input
  
  role = case matches.length
  when 0
    raise QB::Role::NoMatchesError.new input
  when 1
    matches[0]
  else
    raise QB::Role::MultipleMatchesError.new input, matches
  end
  
  QB.debug "role match" => role
  
  role
end
reset_path!() click to toggle source

Reset {QB::Role::PATH} to the original built-in values in {QB::Role::BUILTIN_PATH}.

Created for testing but might be useful elsewhere as well.

@return [Array<String>]

The reset {QB::Role::PATH}.
# File lib/qb/role/search_path.rb, line 139
def self.reset_path!
  PATH.clear
  BUILTIN_PATH.each { |path| PATH << path }
  PATH
end
role_dir?(pathname) click to toggle source

true if pathname is a QB role directory.

# File lib/qb/role.rb, line 45
def self.role_dir? pathname
  # must be a directory
  pathname.directory? &&
  # and must have meta/qb.yml or meta/qb file
  ['qb.yml', 'qb'].any? {|filename| pathname.join('meta', filename).file?}
end
search_path() click to toggle source

Gets the array of paths to search for QB roles based on {QB::Role::PATH} and the working directory at the time it's called.

QB then uses the returned value to figure out what roles are available.

The process:

  1. Resolve relative paths against the working directory.

  2. Load up any `ansible.cfg` files on the path and add any `roles_path` they define where the `ansible.cfg` entry was in {QB::Role::PATH}.

@return [Array<Pathname>]

Directories to search for QB roles.
# File lib/qb/role/search_path.rb, line 161
def self.search_path
  QB::Role::PATH.
    map { |path|
      if QB::Ansible::ConfigFile.end_with_config_file?(path)
        if File.file?(path)
          QB::Ansible::ConfigFile.new(path).defaults.roles_path
        end
      else
        QB::Util.resolve path
      end
    }.
    flatten.
    reject(&:nil?)
end
to_display_path(path) click to toggle source

The path we display in the CLI, see {#display_path}.

@param [Pathname | String] path

input path to transform.

@return [Pathname]

path to display.
# File lib/qb/role.rb, line 162
def self.to_display_path path
  if path.realpath.start_with? QB::GEM_ROLES_DIR
    path.realpath.sub (QB::GEM_ROLES_DIR.to_s + '/'), ''
  else
    QB::Util.contract_path path
  end
end

Public Instance Methods

==(other) click to toggle source
# File lib/qb/role.rb, line 585
def == other
  other.is_a?(self.class) && other.path.realpath == path.realpath
end
Also aliased as: eql?
ask_vault_pass?() click to toggle source

should qb ask for an ansible vault password?

@see docs.ansible.com/ansible/playbooks_vault.html

@return [Boolean]

`true` if qb should ask for a vault password.
# File lib/qb/role.rb, line 479
def ask_vault_pass?
  !!@meta['ask_vault_pass']
end
banner() click to toggle source

get the CLI banner for the role

check_requirements() click to toggle source

Check the role's requirements.

@return [nil]

@raise [QB::AnsibleVersionError]

If the version of Ansible found does not satisfy the role's requirements.

@raise [QB::QBVersionError]

If the the version of QB we're running does not satisfy the role's
requirements.
# File lib/qb/role.rb, line 528
  def check_requirements
    if ansible_req = requirements['ansible']
      unless ansible_req.satisfied_by? QB.ansible_version
        raise QB::AnsibleVersionError.squished <<-END
          QB #{ QB::VERSION } requires Ansible #{ ansible_req },
          found version #{ QB.ansible_version  } at #{ `which ansible` }
        END
      end
    end
    
    if qb_req = requirements.dig( 'gems', 'qb' )
      unless qb_req.satisfied_by? QB.gem_version
        raise QB::QBVersionError.squished <<-END
          Role #{ self } requires QB #{ qb_req },
          using QB #{ QB.gem_version } from #{ QB::ROOT }.
        END
      end
    end
    
    nil
  end
default_ansible_options() click to toggle source

@return [Hash<String, *>]

default `ansible-playbook` CLI options from role qb metadata.
Hash of option name to value.
# File lib/qb/role.rb, line 497
def default_ansible_options
  meta_or 'ansible_options', {}
end
default_dir(cwd, options) click to toggle source

Gets the default `qb_dir` value, raising an error if the role doesn't define how to get one or there is a problem getting it.

It uses a “strategy” value found at the 'default_dir' key in the role's QB metadata (in `<role_path>/meta/qb.yml` or returned by a `<role_path>/meta/qb` executable).

See the {file:doc/qb_roles/metadata/default_dir.md default_dir} documentation for details on the accepted strategy values.

@param [String | Pathname] cwd:

The working directory the CLI command was run in.

@param [Hash<String, QB::Options::Option>] options:

The role options (from {QB::Options#role_options}).

TODO rename this.

@return [Pathname]

The directory to target.

@raise

When we can't determine a directory due to role meta settings or target
system state.
# File lib/qb/role/default_dir.rb, line 71
def default_dir cwd, options
  logger.debug "CALLING default_dir",
    role: self.instance_variables.assoc_to { |key|
      self.instance_variable_get key
    },
    cwd: cwd,
    options: options
  
  default_dir_for(
    strategy: self.meta['default_dir'],
    cwd: cwd,
    options: options
  ).to_pn
end
defaults() click to toggle source

gets the role variable defaults from defaults/main.yml, or {}

# File lib/qb/role.rb, line 362
def defaults
  @defaults || load_defaults
end
description() click to toggle source

@return [String]

The `description` value from the role's QB metadata, or '' if it doesn't
have one
# File lib/qb/role.rb, line 554
def description
  meta['description'].to_s
end
display_name() click to toggle source

Just a string version of {#display_path}

# File lib/qb/role.rb, line 252
def display_name
  display_path.to_s
end
eql?(other)
Alias for: ==
examples() click to toggle source
# File lib/qb/role.rb, line 432
def examples
  @meta['examples']
end
format_examples() click to toggle source

format the `meta.examples` hash into a string suitable for cli output.

@return [String]

the CLI-formatted examples.
# File lib/qb/role.rb, line 442
def format_examples
  examples.
    map {|title, body|
      [
        "#{ title }:",
        body.lines.map {|l|
          # only indent non-empty lines
          # makes compacting newline sequences easier (see below)
          if l.match(/^\s*$/)
            l
          else
            '  ' + l
          end
        },
        ''
      ]
    }.
    flatten.
    join("\n").
    # compact newline sequences
    gsub(/\n\n+/, "\n\n")
end
has_dir_arg?() click to toggle source

Test if the {QB::Role} uses a directory argument (that gets assigned to the `qb_dir` variable in Ansible).

@return [Boolean]

# File lib/qb/role.rb, line 489
def has_dir_arg?
  meta['default_dir'] != false
end
hash() click to toggle source

Language Inter-Op


# File lib/qb/role.rb, line 580
def hash
  path.realpath.hash
end
load_defaults(cache = true) click to toggle source

loads the defaults from vars/main.yml and defaults/main.yml, caching by default. vars override defaults values.

# File lib/qb/role.rb, line 337
def load_defaults cache = true
  defaults_path = @path + 'defaults' + 'main.yml'
  defaults = if defaults_path.file?
    YAML.load(defaults_path.read) || {}
  else
    {}
  end
  
  vars_path = @path + 'vars' + 'main.yml'
  vars = if vars_path.file?
    YAML.load(vars_path.read) || {}
  else
    {}
  end
  
  defaults = defaults.merge! vars
  
  if cache
    @defaults = defaults
  end
  
  defaults
end
load_meta(cache = true) click to toggle source

load qb metadata from meta/qb.yml or from executing meta/qb and parsing the YAML written to stdout.

if `cache` is true caches it as `@meta`

# File lib/qb/role.rb, line 266
def load_meta cache = true
  meta = if @meta_path.extname == '.yml'
    contents = begin
      @meta_path.read
    rescue Exception => error
      raise QB::Role::MetadataError,
        "Failed to read metadata file at #{ @meta_path.to_s }, " +
        "error: #{ error.inspect }"
    end
    
    begin
      YAML.load(contents) || {}
    rescue Exception => error
      raise QB::Role::MetadataError,
        "Failed to load metadata YAML from #{ @meta_path.to_s }, " +
        "error: #{ error.inspect }"
    end
  else
    YAML.load(Cmds.out!(@meta_path.realpath.to_s)) || {}
  end
  
  if cache
    @meta = meta
  end
  
  meta
end
meta() click to toggle source

@return [Hash{String => Object}]

the QB metadata for the role.
# File lib/qb/role.rb, line 297
def meta
  @meta || load_meta
end
mkdir() click to toggle source

if the exe should auto-make the directory. this is nice for most roles but some need it to be missing

# File lib/qb/role.rb, line 372
def mkdir
  !!meta_or('mkdir', true)
end
namespace() click to toggle source

Instance Methods

# File lib/qb/role/name.rb, line 128
def namespace
  self.class.namespace_for @name
end
namespaceless() click to toggle source
# File lib/qb/role/name.rb, line 133
def namespaceless
  self.class.namespaceless_for @name
end
option_metas() click to toggle source

get the options from the metadata, defaulting to [] if none defined

# File lib/qb/role.rb, line 314
def option_metas
  meta_or ['options', 'opts', 'vars'], []
end
options(include_path = []) click to toggle source

@return [Array<QB::Options::Option>

an array of Option for the role, including any included roles.
# File lib/qb/role.rb, line 322
def options include_path = []
  option_metas.map {|option_meta|
    if option_meta.key? 'include'
      role_name = option_meta['include']
      role = QB::Role.require role_name
      role.options QB::Role.get_include_path(role, option_meta, include_path)
    else
      QB::Options::Option.new self, option_meta, include_path
    end
  }.flatten
end
options_key() click to toggle source
# File lib/qb/role.rb, line 257
def options_key
  display_name
end
puts_examples() click to toggle source

examples text

# File lib/qb/role.rb, line 466
def puts_examples
  return unless examples
  
  puts "\n" + format_examples + "\n"
end
requirements() click to toggle source

Parsed tree structure of version requirements of the role from the `requirements` value in the QB meta data.

@return [Hash]

Tree where the leaves are {Gem::Requirement}.
# File lib/qb/role.rb, line 508
def requirements
  @requirements ||= NRSER.map_leaves(
    meta_or 'requirements', {'gems' => {}}
  ) { |key_path, req_str|
    Gem::Requirement.new req_str
  }
end
save_options() click to toggle source
# File lib/qb/role.rb, line 366
def save_options
  !!meta_or('save_options', true)
end
summary() click to toggle source

Short summary pulled from the role description - first line if it's multi-line, or first sentence if it's a single line.

Will be an empty string if the role doesn't have a description.

@return [String]

# File lib/qb/role.rb, line 566
def summary
  description.lines.first.thru { |line|
    if line
      line.split( '. ', 2 ).first
    else
      ''
    end
  }
end
to_s() click to toggle source

@return [String]

{QB::Role#display_path}
# File lib/qb/role.rb, line 595
def to_s
  @display_path.to_s
end
usage() click to toggle source

@return [String]

usage information formatted as plain text for the CLI.
# File lib/qb/role.rb, line 379
def usage
  # Split up options by required and optional.
  required_options = []
  optional_options = []
  
  options.each { |option|
    if option.required?
      required_options << option
    else
      optional_options << option
    end
  }
  
  parts = ['qb [run]', name]
  
  required_options.each { |option|
    parts << option.usage
  }
  
  unless optional_options.empty?
    parts << '[OPTIONS]'
  end
  
  if has_dir_arg?
    parts << 'DIRECTORY'
  end
  
  parts.join ' '
end
var_prefix() click to toggle source

gets the variable prefix that will be appended to cli options before passing them to the role. defaults to `#namespaceless` unless specified in meta.

# File lib/qb/role.rb, line 305
def var_prefix
  # ugh, i was generating meta/qb.yml files that set 'var_prefix' to
  # `null`, but it would be nice to
  #
  meta_or 'var_prefix', namespaceless
end

Protected Instance Methods

default_dir_for(strategy:, cwd:, options: case strategy) click to toggle source

Internal, possibly recursive method that actually does the work of figuring out the directory value.

Recurs when the meta value is an array, trying each of the entries in sequence, returning the first to succeed, and raising if they all fail.

@param [nil | false | String | Hash | Array] strategy

Instruction for how to determine the directory value.

See the {file:doc/qb_roles.md#default_dir default_dir} for a details
on recognized values.

@return [return_type]

@todo Document return value.
# File lib/qb/role/default_dir.rb, line 105
    def default_dir_for strategy:, cwd:, options:
      case strategy
      when nil
        # there is no get_dir info in meta/qb.yml, can't get the dir
        raise QB::UserInputError.new binding.erb <<-END
          No default directory for role <%= self.name %>
          
          Role <%= self.name %> does not provide a default target directory
          (used to populate the `qb_dir` Ansible variable).
          
          You must provide one via the CLI like
          
              qb run <%= self.name %> DIRECTORY
          
          or, if you are the developer of the <%= self.name %> role, set a
          non-null value for the 'default_dir' key in
          
              <%= self.meta_path %>
          
        END
      
      when false
        # this method should not get called when the strategy is `false` (an
        # entire section is skipped in exe/qb when `default_dir = false`)
        raise QB::StateError.squished <<-END
          role does not use default directory (meta/qb.yml:default_dir = false)
        END
      
      when 'git_root'
        logger.debug "returning the git root relative to cwd"
        NRSER.git_root cwd
      
      when 'cwd'
        logger.debug "returning current working directory"
        cwd
        
      when Hash
        logger.debug "qb meta option is a Hash"
        
        unless strategy.length == 1
          raise "#{ meta_path.to_s }:default_dir invalid: #{ strategy.inspect }"
        end
        
        hash_key, hash_value = strategy.first
        
        case hash_key
        when 'exe'
          exe_path = hash_value
          
          # supply the options to the exe so it can make work off those values
          # if it wants.
          exe_input_data = Hash[
            options.map {|option|
              [option.cli_option_name, option.value]
            }
          ]
          
          unless exe_path.start_with?('~') || exe_path.start_with?('/')
            exe_path = File.join(self.path, exe_path)
            debug 'exe path is relative, basing off role dir', exe_path: exe_path
          end
          
          debug "found 'exe' key, calling", exe_path: exe_path,
                                            exe_input_data: exe_input_data
          
          Cmds.chomp! exe_path do
            JSON.dump exe_input_data
          end
          
        when 'find_up'
          rel_path = hash_value
          
          unless rel_path.is_a? String
            raise "find_up relative path or glob must be string, found #{ rel_path.inspect }"
          end
          
          logger.debug "found 'find_up' strategy", rel_path: rel_path
          
          cwd.to_pn.find_up! rel_path
        
        when 'from_role'
          # Get the value from another role, presumably one this role includes
          
          default_dir_for \
            strategy: QB::Role.require( hash_value ).meta['default_dir'],
            cwd: cwd,
            options: options
          
        else
          raise QB::Role::MetadataError.new binding.erb <<-END
            Bad key <%= hash_key.inspect %> in 'default_dir' value
            
            Metadata for role <%= name %> read from
            
                <%= self.meta_path.to_s %>
            
            contains an invalid default directory strategy
            
                <%= strategy.pretty_inspect %>
            
            The key <%= hash_key.inspect %> does not correspond to a recognized
            form.
            
            Valid forms are:
            
            1.  {exe:         FILEPATH}
            2.  {file_up:     FILEPATH}
            3.  {from_role:   ROLE}
            
          END
        end
      
      when Array
        strategy.try_find do |candidate|
          default_dir_for strategy: candidate, cwd: cwd, options: options
        end
      
      else
        raise QB::Role::MetadataError.new binding.erb <<-END
          bad default_dir strategy: <%= strategy %>
        END
      end

Private Instance Methods

meta_or(keys, default) click to toggle source

get the value at the first found of the keys or the default.

`nil` (`null` in yaml files) are treated like they're not there at all. you need to use `false` if you want to tell QB not to do something.

@param [String | Symbol | Array<String | Symbol>] keys

Single

@return [Object]

# File lib/qb/role.rb, line 613
def meta_or keys, default
  keys.as_array.map(&:to_s).each do |key|
    return meta[key] unless meta[key].nil?
  end
  
  # We didn't find anything (that wasn't explicitly or implicitly `nil`)
  default
end
need_meta(keys) click to toggle source

Find a non-null/nil value for one of `keys` or raise an error.

@param [String | Symbol | Array<String | Symbol>] keys

Possible key names. They will be searched in order and first
non-null/nil value returned.

@return [Object]

@raise [QB::Role::MetadataError]

If none of `keys` are found.
# File lib/qb/role.rb, line 634
    def need_meta keys
      keys = keys.as_array.map(&:to_s)
      
      keys.each do |key|
        return meta[key] unless meta[key].nil?
      end
      
      raise QB::Role::MetadataError.squished <<-END
        Expected metadata for role #{ self } to define (non-null) value for
        one of keys #{ keys.inspect }.
      END
    end