module NRSER

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Functions for associating entries in an {Enumerable} as key or values in a {Hash}.

Definitions

Definitions

Definitions

Definitions

Namespace

Namespace

Definitions

Functional methods to stylize a string through substitution

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Declarations

Namespace

Namespace

Namespace

Declarations

Declarations

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Constants

FALSY_STRINGS

Down-cased versions of strings that are considered to communicate false in things like ENV vars, CLI options, etc.

@return [Set<String>]

INDENT_RE

Constants

INDENT_TAG_MARKER
INDENT_TAG_SEPARATOR
JSON_ARRAY_RE

Regexp used to guess if a string is a JSON-encoded array.

@return [Regexp]

JSON_OBJECT_RE

Regexp used to guess if a string is a JSON-encoded object.

@return [Regexp]

NO_ARG
ROOT

Absolute, expanded path to the gem's root directory.

Here in `//lib/nrser/version` so that it can be used via

require 'nrser/version'

without loading the entire module.

@return [Pathname]

SPLIT_WORDS_RE

Regexp {NRSER.words} uses to split strings. Probably not great but it's what I have for the moment.

@return {Regexp}

TRUTHY_STRINGS

Down-cased versions of strings that are considered to communicate true in things like ENV vars, CLI options, etc.

@return [Set<String>]

UNICODE_ELLIPSIS

Unicode ellipsis character.

@return [String]

VERSION

String version of the gem.

@return [String]

WHITESPACE_RE

Regular expression used to match whitespace.

@return [Regexp]

Public Class Methods

_to_open_struct(value, freeze: result = case value) click to toggle source
# File lib/nrser/functions/open_struct.rb, line 22
def self._to_open_struct value, freeze:
  result = case value
  when OpenStruct
    # Just assume it's already taken care of if it's already an OpenStruct
    value
    
  when Hash
    OpenStruct.new(
      value.transform_values { |v| _to_open_struct v, freeze: freeze }
    )
    
  when Array
    value.map { |v| _to_open_struct v, freeze: freeze }
    
  when Set
    Set.new value.map { |v| _to_open_struct v, freeze: freeze }
  
  else
    value
  end
array_include_slice?(enum, slice, &is_match) click to toggle source

Test slice inclusion when both the `slice` and the `enum` that we're going to look for it in support `#length` and `#slice` in the same manner that {Array} does (hence the name).

This is much simpler and more efficient than the “general” {Enumerable} case where we can't necessarily find out how many entries are in the enumerables or really do much of anything with them except iterate through (at least, with my current grasp of {Enumerable} and {Enumerator} it seems painfully complex… in fact it may never terminate for infinite enumerables).

@param [Enumerable<E> & length & slice] enum

The {Enumerable} that we want test for `slice` inclusion. Must
support `#length` and `#slice` like {Array} does.

@param [Enumerable<S> & length & slice] slice

The {Enumerable} slice that we want to see if `enum` includes. Must
support `#length` and `#slice` like {Array} does.

@param [Proc<(E, S)=>Boolean>] is_match

Optional {Proc} that accepts an entry from `enum` and an entry from
`slice` and returns if they match.

@return [Boolean]

`true` if there is a slice of `enum` for which each entry matches the
corresponding entry in `slice` according to `&is_match`.
# File lib/nrser/functions/enumerable/include_slice/array_include_slice.rb, line 33
def self.array_include_slice? enum, slice, &is_match
  slice_length = slice.length
  
  # Short-circuit on empty slice - it's *always* present
  return true if slice_length == 0
  
  enum_length = enum.length
  
  # Short-circuit if slice is longer than enum since we can't possibly
  # match
  return false if slice_length > enum_length
  
  # Create a default `#==` matcher if we weren't provided one.
  if is_match.nil?
    is_match = ->(enum_entry, slice_entry) {
      enum_entry == slice_entry
    }
  end
  
  enum.each_with_index do |enum_entry, enum_start_index|
    # Compute index in `enum` that we would need to match up to
    enum_end_index = enum_start_index + slice_length - 1
    
    # Short-circuit if can't match (more slice entries than enum ones left)
    return false if enum_end_index >= enum_length
    
    # Create the slice to test against
    enum_slice = enum[enum_start_index..enum_end_index]
    
    # See if every entry in the slice from `enum` matches the corresponding
    # one in `slice`
    return true if enum_slice.zip( slice ).all? { |(enum_entry, slice_entry)|
      is_match.call enum_entry, slice_entry
    }
    
    # Otherwise, just continue on through `enum` looking for that first
    # match until the number of `enum` entries left is too few for `slice`
    # to possibly match
  end
  
  # We never matched the first `slice` entry to a `enum` entry (and `slice`
  # had to be of length 1 so that the "too long" short-circuit never fired).
  #
  # So, we don't have a match.
  false
end
array_like?(object) click to toggle source

Test if an object is “array-like” - is it an Enumerable and does it respond to `#each_index`?

@param [Object] object

Any old thing.

@return [Boolean]

`true` if `object` is "array-like" for our purposes.
# File lib/nrser/functions/enumerable.rb, line 18
def self.array_like? object
  object.is_a?( ::Enumerable ) &&
    object.respond_to?( :each_index )
end
as_array(value) click to toggle source

Return an array given any value in the way that makes most sense:

  1. If `value` is an array, return it.

  2. If `value` is `nil`, return `[]`.

  3. If `value` responds to `#to_a`, try calling it. If it succeeds, return that.

  4. Return an array with `value` as it's only item.

Refinement


Added to `Object` in `nrser/refinements`.

@param [Object] value

@return [Array]

# File lib/nrser/functions/object/as_array.rb, line 25
def self.as_array value
  return value if value.is_a? Array
  return [] if value.nil?
  
  if value.respond_to? :to_a
    begin
      return value.to_a
    rescue
    end
  end
  
  [value]
end
as_hash(value, key = nil) click to toggle source

Treat the value as the value for `key` in a hash if it's not already a hash and can't be converted to one:

  1. If the value is a `Hash`, return it.

  2. If `value` is `nil`, return `{}`.

  3. If the value responds to `#to_h` and `#to_h` succeeds, return the resulting hash.

  4. Otherwise, return a new hash where `key` points to the value. **`key` MUST be provided in this case.**

Useful in method overloading and similar situations where you expect a hash that may specify a host of options, but want to allow the method to be called with a single value that corresponds to a default key in that option hash.

Refinement


Added to `Object` in `nrser/refinements`.

Example Time!


Say you have a method `m` that handles a hash of HTML options that can look something like

{class: 'address', data: {confirm: 'Really?'}}

And can call `m` like

m({class: 'address', data: {confirm: 'Really?'}})

but often you are just dealing with the `:class` option. You can use {NRSER.as_hash} to accept a string and treat it as the `:class` key:

using NRSER

def m opts
  opts = opts.as_hash :class
  # ...
end

If you pass a hash, everything works normally, but if you pass a string `'address'` it will be converted to `{class: 'address'}`.

About `#to_h` Support


Right now, {.as_hash} also tests if `value` responds to `#to_h`, and will try to call it, using the result if it doesn't raise. This lets it deal with Ruby's “I used to be a Hash until someone mapped me” values like `[[:class, 'address']]`. I'm not sure if this is the best approach, but I'm going to try it for now and see how it pans out in actual usage.

@todo

It might be nice to have a `check` option that ensures the resulting
hash has a value for `key`.

@param [Object] value

The value that we want to be a hash.

@param [Object] key [default nil]

The key that `value` will be stored under in the result if `value` is
not a hash or can't be turned into one via `#to_h`. If this happens
this value can **NOT** be `nil` or an `ArgumentError` is raised.

@return [Hash]

@raise [ArgumentError]

If it comes to constructing a new Hash with `value` as a value and no
argument was provided
# File lib/nrser/functions/object/as_hash.rb, line 82
def self.as_hash value, key = nil
  return value if value.is_a? Hash
  return {} if value.nil?
  
  if value.respond_to? :to_h
    begin
      return value.to_h
    rescue
    end
  end
  
  # at this point we need a key argument
  if key.nil?
    raise ArgumentError,
          "Need key to construct hash with value #{ value.inspect }, " +
          "found nil."
  end
  
  {key => value}
end
assoc_by(enum, &block) click to toggle source

Convert an enumerable to a hash by passing each entry through `&block` to get it's key, raising an error if multiple entries map to the same key.

@example Basic usage

['a', :b].assoc_by &:class
# => {String=>"a", Symbol=>:b}

@example Conflict error

[:a, :b].assoc_by &:class
# NRSER::ConflictError: Key Symbol is already in results with value:
#
#     :a
#

@param [Enumerable<V>] enum

Enumerable containing the values for the hash.

@param [Proc<(V)=>K>] block

Block that maps `enum` values to their hash keys.

@return [Hash<K, V>]

@raise [NRSER::ConflictError]

If two values map to the same key.
# File lib/nrser/functions/enumerable/associate.rb, line 35
  def self.assoc_by enum, &block
    enum.each_with_object( {} ) { |element, result|
      key = block.call element
      
      if result.key? key
        raise NRSER::ConflictError.new binding.erb <<-END
          Key <%= key.inspect %> is already in results with value:
          
              <%= result[key].pretty_inspect %>
          
        END
      end
      
      result[key] = element
    }
  end
assoc_to(enum, on_conflict: :raise, &block) click to toggle source

Create a {Hash} mapping the entries in `enum` to the value returned by passing them through `&block`, raising on conflicts.

@param [Enumerable<ENTRY>] enum

@param [ :raise | :first_wins | :last_wins | Proc ] on_conflict

What to do when there's a conflict mapping the entries into the hash.

The names are meant to make some sense.

@param [Proc<(ENTRY)=>VALUE>] block

The star of the show! Maps `ENTRY` from `enum` to `VALUE` for the
resulting hash.

@return [Hash<ENTRY, VALUE>]

@raise [NRSER::ConflictError]

If a conflict occurs and `on_conflict` is set to `:raise`.
# File lib/nrser/functions/enumerable/associate.rb, line 72
  def self.assoc_to enum, on_conflict: :raise, &block
    enum.each_with_object( {} ) { |entry, hash|
      value = if hash.key? entry
        case on_conflict
        when :raise
          raise NRSER::ConflictError.new binding.erb <<-END
            Entry <%= entry %> appears more than once in `enum`
            
            This would cause conflict in the resulting {Hash}.
            
            Entry:
            
                <%= entry.pretty_inspect %>
            
          END
        when :first_wins
          # do nothing
        when :last_wins
          hash[entry] = block.call entry
        when Proc
          hash[entry] = on_conflict.call \
            entry: entry,
            current_value: hash[entry],
            block: block
        else
          raise ArgumentError,
            "Bad `on_conflict`: #{ on_conflict.inspect }"
        end
      else
        block.call entry
      end
      
      hash[entry] = value
    }
  end
bury!(hash, key_path, value, parsed_key_type: :guess, clobber: false, create_arrays_for_unsigned_keys: false) click to toggle source

The opposite of `#dig` - set a value at a deep key path, creating necessary structures along the way and optionally clobbering whatever's in the way to achieve success.

@param [Hash] hash

Hash to bury the value in.

@param [Array | to_s] key_path

-   When an {Array}, each entry is used exactly as-is for each key.

-   Otherwise, the `key_path` is converted to a string and split by
    `.` to produce the key array, and the actual keys used depend on
    the `parsed_key_type` option.

@param [Object] value

The value to set at the end of the path.

@param [Class | :guess] parsed_key_type

How to handle parsed key path segments:

-   `String` - use the strings that naturally split from a parsed
    key path.

    Note that this is the *String class itself, **not** a value that
    is a String*.

-   `Symbol` - convert the strings that are split from the key path
    to symbols.

    Note that this is the *Symbol class itself, **not** a value that
    is a Symbol*.``

-   `:guess` (default) -

@return [return_type]

@todo Document return value.
# File lib/nrser/functions/hash/bury.rb, line 45
def self.bury! hash,
          key_path,
          value,
          parsed_key_type: :guess,
          clobber: false,
          create_arrays_for_unsigned_keys: false
  
  # Parse the key if it's not an array
  unless key_path.is_a?( Array )
    key_path = key_path.to_s.split '.'
    
    # Convert the keys to symbols now if that's what we want to use
    if parsed_key_type == Symbol
      key_path.map! &:to_sym
    end
  end
  
  _internal_bury! \
    hash,
    key_path,
    value,
    guess_key_type: ( parsed_key_type == :guess ),
    clobber: clobber,
    create_arrays_for_unsigned_keys: create_arrays_for_unsigned_keys
end
chainer(mappable, publicly: true) click to toggle source

Map *each entry* in `mappable` to a {NRSER::Message} and return a {Proc} that accepts a single `receiver` argument and reduces it by applying each message in turn.

In less precise terms: create a proc that chains the entries as methods calls.

@note

`mappable`` entries are mapped into messages when {#to_chain} is called,
meaning subsequent changes to `mappable` **will not** affect the
returned proc.

@example Equivalent of `Time.now.to_i`

NRSER::chainer( [:now, :to_i] ).call Time
# => 1509628038

@return [Proc]

# File lib/nrser/functions/proc.rb, line 86
def self.chainer mappable, publicly: true
  messages = mappable.map { |value| message *value }
  
  ->( receiver ) {
    messages.reduce( receiver ) { |receiver, message|
      message.send_to receiver, publicly: publicly
    }
  }
end
collection?(obj) click to toggle source

test if an object is considered a collection.

@param obj [Object] object to test @return [Boolean] true if `obj` is a collection.

# File lib/nrser/collection.rb, line 25
def collection? obj
  Collection::STDLIB.any? {|cls| obj.is_a? cls} || obj.is_a?(Collection)
end
common_prefix(strings) click to toggle source
# File lib/nrser/functions/string.rb, line 44
def self.common_prefix strings
  raise ArgumentError.new("argument can't be empty") if strings.empty?
  
  sorted = strings.sort
  
  i = 0
  
  while sorted.first[i] == sorted.last[i] &&
        i < [sorted.first.length, sorted.last.length].min
    i = i + 1
  end
  
  sorted.first[0...i]
end
count_by(enum, &block) click to toggle source

Count entries in an {Enumerable} by the value returned when they are passed to the block.

@example Count array entries by class

[1, 2, :three, 'four', 5, :six].count_by &:class
# => {Fixnum=>3, Symbol=>2, String=>1}

@param [Enumerable<E>] enum

{Enumerable} (or other object with compatible `#each_with_object` and
`#to_enum` methods) you want to count.

@param [Proc<(E)=>C>] block

Block mapping entries in `enum` to the group to count them in.

@return [Hash{C=>Integer}]

Hash mapping groups to positive integer counts.
# File lib/nrser/functions/enumerable.rb, line 203
def self.count_by enum, &block
  enum.each_with_object( Hash.new 0 ) do |entry, hash|
    hash[block.call entry] += 1
  end
end
dedent(text, ignore_whitespace_lines: true, return_lines: false) click to toggle source
# File lib/nrser/functions/text/indentation.rb, line 55
def self.dedent text,
                ignore_whitespace_lines: true,
                return_lines: false
  return text if text.empty?
  
  all_lines = if text.is_a?( Array )
    text
  else
    text.lines
  end
  
  indent_significant_lines = if ignore_whitespace_lines
    all_lines.reject { |line| whitespace? line }
  else
    all_lines
  end
  
  indent = find_indent indent_significant_lines
  
  return text if indent.empty?
  
  dedented_lines = all_lines.map { |line|
    if line.start_with? indent
      line[indent.length..-1]
    elsif line.end_with? "\n"
      "\n"
    else
      ""
    end
  }
  
  if return_lines
    dedented_lines
  else
    dedented_lines.join
  end
end
dir_from(path) click to toggle source

Get the directory for a path - if the path is a directory, it's returned (converted to a {Pathname}). It's not a directory, it's {Pathname#dirname} will be returned.

Expands the path (so that `~` paths work).

@param [String | Pathname] path

File or directory path.

@return [Pathname]

Absolute directory path.
# File lib/nrser/functions/path.rb, line 53
def self.dir_from path
  pn = pn_from( path ).expand_path
  
  if pn.directory?
    pn
  else
    pn.dirname
  end
end
each(object, &block) click to toggle source

Yield on each element of a collection or on the object itself if it's not a collection. avoids having to normalize to an array to iterate over something that may be an object OR a collection of objects.

NOTE Implemented for our idea of a collection instead of testing

for response to `#each` (or similar) to avoid catching things
like {IO} instances, which include {Enumerable} but are
probably not what is desired when using {NRSER.each}
(more likely that you mean "I expect one or more files" than
"I expect one or more strings which may be represented by
lines in an open {File}").

@param [Object] object

Target object.

@yield

Each element of a collection or the target object itself.

@return [Object]

`object` param.
# File lib/nrser/collection.rb, line 51
    def each object, &block
      if collection? object
        # We need to test for response because {OpenStruct} *will* respond to
        # #each because *it will respond to anything* (which sucks), but it
        # will return `false` for `respond_to? :each` and the like, and this
        # behavior could be shared by other collection objects, so it seems
        # like a decent idea.
        if object.respond_to? :each_pair
          object.each_pair &block
        elsif object.respond_to? :each
          object.each &block
        else
          raise TypeError.squished <<-END
            Object #{ obj.inpsect } does not respond to #each or #each_pair
          END
        end
      else
        block.call object
      end
      object
    end
each_branch(tree, &block) click to toggle source

Enumerate over the immediate “branches” of a structure that can be used to compose our idea of a tree: nested hash-like and array-like structures like you would get from parsing a JSON document.

Written and tested against Hash and Array instances, but should work with anything hash-like that responds to `#each_pair` appropriately or array-like that responds to `#each_index` and `#each_with_index`.

@note Not sure what will happen if the tree has circular references!

@param [#each_pair | (each_index & each_with_index)] tree

Structure representing a tree via hash-like and array-like containers.

@yieldparam [Object] key

The first yielded param is the key or index for the value branch at the
top level of `tree`.

@yieldparam [Object] value

The second yielded param is the branch at the key or index at the top
level of `tree`.

@yieldreturn

Ignored.

@return [Enumerator]

If no block is provided.

@return [#each_pair | (each_index & each_with_index)]

If a block is provided, the result of the `#each_pair` or
`#each_with_index` call.

@raise [NoMethodError]

If `tree` does not respond to `#each_pair` or to `#each_index` and
`#each_with_index`.
# File lib/nrser/functions/tree/each_branch.rb, line 40
  def self.each_branch tree, &block
    if tree.respond_to? :each_pair
      # Hash-like
      tree.each_pair &block
      
    elsif tree.respond_to? :each_index
      # Array-like... we test for `each_index` because - unintuitively -
      # `#each_with_index` is a method of {Enumerable}, meaning that {Set}
      # responds to it, though sets are unordered and the values can't be
      # accessed via those indexes. Hence we look for `#each_index`, which
      # {Set} does not respond to.
      
      if block.nil?
        index_enumerator = tree.each_with_index
        
        Enumerator.new( index_enumerator.size ) { |yielder|
          index_enumerator.each { |value, index|
            yielder.yield [index, value]
          }
        }
      else
        tree.each_with_index.map { |value, index|
          block.call [index, value]
        }
      end
      
    else
      raise NoMethodError.new NRSER.squish <<-END
        `tree` param must respond to `#each_pair` or `#each_index`,
        found #{ tree.inspect }
      END
      
    end # if / else
  end
ellipsis(source, max, omission: UNICODE_ELLIPSIS) click to toggle source

Cut the middle out of a sliceable object with length and stick an ellipsis in there instead.

Categorized with {String} functions 'cause that's where it started, and that's probably how it will primarily continue to be used, but tested to work on {Array} and should for other classes that satisfy the same slice and interface.

@param [V & length & slice & << & +] source

Source object. In practice, {String} and {Array} work. In theory,
anything that responds to `#length`, `#slice`, `#<<` and `#+` with the
same semantics will work.

@param [Fixnum] max

Max length to allow for the output string.

@param [String] omission

The string to stick in the middle where original contents were
removed. Defaults to the unicode ellipsis since I'm targeting the CLI
at the moment and it saves precious characters.

@return [V]

Object of the same type as `source` of at most `max` length with the
middle chopped out if needed to do so.\*

\* Really, it has to do with how all the used methods are implemented,
but we hope that conforming classes will return instances of their own
class like {String} and {Array} do.
# File lib/nrser/functions/string.rb, line 128
def self.ellipsis source, max, omission: UNICODE_ELLIPSIS
  return source unless source.length > max
  
  trim_to = max - ( String === source ? omission.length : 1 )
  middle = trim_to / 2
  remainder = trim_to % 2
  
  start = source.slice( 0, middle + remainder )
  start << omission
  
  finish = source.slice( -( middle - remainder )..-1 )
  
  start + finish
end
enumerate_as_values(enum) click to toggle source

Create an {Enumerator} that iterates over the “values” of an {Enumerable} `enum`. If `enum` responds to `#each_value` than we return that. Otherwise, we return `#each_entry`.

@param [Enumerable] enum

@return [Enumerator]

@raise [ArgumentError]

If `enum` doesn't respond to `#each_value` or `#each_entry`.
# File lib/nrser/functions/enumerable.rb, line 165
  def self.enumerate_as_values enum
    # NRSER.match enum,
    #   t.respond_to(:each_value), :each_value.to_proc,
    #   t.respond_to(:each_entry), :each_entry.to_proc
    #
    if enum.respond_to? :each_value
      enum.each_value
    elsif enum.respond_to? :each_entry
      enum.each_entry
    else
      raise ArgumentError.new erb binding, <<-END
        Expected `enum` arg to respond to :each_value or :each_entry, found:
        
            <%= enum.inspect %>
        
      END
    end
  end
extract_from_array!(array, &block) click to toggle source

A destructive partition.

# File lib/nrser/functions/array.rb, line 17
def self.extract_from_array! array, &block
  extracted = []
  array.reject! { |entry|
    test = block.call entry
    if test
      extracted << entry
    end
    test
  }
  extracted
end
falsy?(object) click to toggle source

Opposite of {NRSER.truthy?}.

@pure Return value depends only on parameters.

@param object (see .truthy?)

@return [Boolean]

The negation of {NRSER.truthy?}.

@raise [ArgumentError]

When a string is received that is not in {NRSER::TRUTHY_STRINGS} or
{NRSER::FALSY_STRINGS} (case insensitive).

@raise [TypeError]

When `object` is not the right type.
# File lib/nrser/functions/object/truthy.rb, line 107
def self.falsy? object
  ! truthy?(object)
end
filter_repeated_blank_lines(str, remove_leading: false) click to toggle source
# File lib/nrser/functions/string.rb, line 60
def self.filter_repeated_blank_lines str, remove_leading: false
  out = []
  lines = str.lines
  skipping = remove_leading
  str.lines.each do |line|
    if line =~ /^\s*$/
      unless skipping
        out << line
      end
      skipping = true
    else
      skipping = false
      out << line
    end
  end
  out.join
end
find_all_map(enum, &block) click to toggle source

Find all truthy (not `nil` or `false`) results of calling `&block` with entries from `enum`.

@example

NRSER.find_all_map( [1, 2, 3, 4] ) do |i|
  if i.even?
    "#{ i } is even!"
  end
end
# => ["2 is even!", "4 is even!"]

@param [Enumerable<E>] enum

Entries to search (in order).

@param [Proc<(E)=>R>] block

Block mapping entires to results.

@return [nil]

When `block.call( E )` is `nil` or `false` for all `E` in `enum`.

@return [R]

The first result `R = block.call( E )` where `R` is not `nil` or `false`.
# File lib/nrser/functions/enumerable/find_all_map.rb, line 29
def self.find_all_map enum, &block
  enum.map( &block ).select { |entry| entry }
end
find_bounded(enum, bounds, &block) click to toggle source

Find all entries in an {Enumerable} for which `&block` returns a truthy value, then check the amount of results found against the {NRSER::Types.length} created from `bounds`, raising a {TypeError} if the results' length doesn't satisfy the bounds type.

@param [Enumerable<E>] enum

The entries to search and check.

@param [Integer | Hash] bounds

Passed as only argument to {NRSER::Types.length} to create the length
type the results are checked against.

@param [Proc] block

`#find`/`#find_all`-style block that will be called with each entry
from `enum`. Truthy responses mean the entry matched.

@return [Array<E>]

Found entries from `enum`.

@raise [TypeError]

If the results of `enum.find_all &block` don't satisfy `bounds`.
# File lib/nrser/functions/enumerable.rb, line 61
  def self.find_bounded enum, bounds, &block
    NRSER::Types.
      length(bounds).
      check(enum.find_all &block) { |type:, value:|
        binding.erb <<-END
          
          Length of found elements (<%= value.length %>) FAILED to
          satisfy <%= type.to_s %>.
          
          Found entries:
          
              <%= value.pretty_inspect %>
          
          from enumerable:
          
              <%= enum.pretty_inspect %>
          
        END
      }
  end
find_indent(text) click to toggle source

Functions

# File lib/nrser/functions/text/indentation.rb, line 17
def self.find_indent text
  common_prefix lines( text ).map { |line| line[INDENT_RE] }
end
find_only(enum, &block) click to toggle source

Find the only entry in `enum` for which `&block` responds truthy, raising if either no entries or more than one are found.

Returns the entry itself, not an array of length 1.

Just calls {NRSER.find_bounded} with `bounds = 1`.

@param enum (see NRSER.find_bounded) @param &block (see NRSER.find_bounded)

@return [E]

Only entry in `enum` that `&block` matched.

@raise [TypeError]

If `&block` matched more or less than one entry.
# File lib/nrser/functions/enumerable.rb, line 99
def self.find_only enum, &block
  find_bounded(enum, 1, &block).first
end
find_up( rel_path, from: Pathname.pwd, glob: :guess, test: :exist?, result: :common_root ) click to toggle source

Ascend the directory tree starting at `from` (defaults to working directory) looking for a relative path.

How it works and what it returns is dependent on the sent options.

In the simplest / default case:

1.

@param [String | Pathname] rel_path

Relative path to search for. Can contains glob patterns; see the `glob`
keyword.

@param [String | Pathname] from

Where to start the search. This is the first directory checked.

@param [Boolean | :guess] glob

Controls file-glob behavior with respect to `rel_path`:

-   `:guess` (default) - boolean value is computed by passing `rel_path`
    to {.looks_globish?}.

-   `true` - {Pathname.glob} is used to search for `rel_path` in each
    directory, and the first glob result that passes the test is
    considered the match.

-   `false` - `rel_path` is used as a literal file path (if it has a `*`
    character it will only match paths with a literal `*` character,
    etc.)

**Be mindful that glob searches can easily consume significant resources
when using broad patterns and/or large file trees.**

Basically, you probably don't *ever* want to use `**` - we walk all the
way up to the file system root, so it would be equivalent to searching
*the entire filesystem*.

@todo

There should be a way to cut the search off early or detect `**` in
the `rel_path` and error out or something to prevent full FS search.

@param [Symbol] test

The test to perform on pathnames to see if they match. Defaults to
`:exist?` - which calls {Pathname#exist?} - but could be `:directory?`
or anything else that makes sense.

@param [Symbol] result

What information to return:

-   `:common_root` (default) - return the directory that the match was
    relative to, so the return value is `from` or a ancestor of it.

-   `:path` - return the full path that was matched.

-   `:pair` - return the `:common_root` value followed by the `:path`
    value in a two-element {Array}.

@return [nil]

When no match is found.

@return [Pathname]

When a match is found and `result` keyword is

-   `:common_root` - the directory in `from.ascend` the match was made
    from.

-   `:path` - the path to the matched file.

@return [Array<(Pathname, Pathname)>]

When a match is found and `result` keyword is `:pair`, the directory
the match was relative to followed by the matched path.
# File lib/nrser/functions/path.rb, line 141
def self.find_up(
  rel_path,
  from: Pathname.pwd,
  glob: :guess,
  test: :exist?,
  result: :common_root
)
  # If `glob` is `:guess`, override `glob` with the result of
  # {.looks_globish?}
  #
  glob = looks_globish?( rel_path ) if glob == :guess
  
  found = pn_from( from ).ascend.find_map { |dir|
    path = dir / rel_path
    
    found_path = if glob
      Pathname.glob( path ).find { |match_path|
        match_path.public_send test
      }
    elsif path.public_send( test )
      path
    else
      nil
    end
    
    unless found_path.nil?
      [dir, found_path]
    end
  }
  
  return nil if found.nil?
  
  dir, path = found
  
  Types.match result,
    :common_root, dir,
    :pair, found,
    :path, path
end
find_up!(*args) click to toggle source

Exactly like {NRSER.find_up} but raises if nothing is found.

# File lib/nrser/functions/path.rb, line 184
def self.find_up! *args
  find_up( *args ).tap { |result|
    if result.nil?
      raise "HERE! #{ args.inspect }"
    end
  }
end
fmt_msg(*segments) click to toggle source

Provides simple formatting for messages constructed as a list of segments.

Allows you to do this sort of thing:

NRSER.fmt_msg "Some stuff went wrong with the", thing,
  "and we're figuring it out, sorry. Maybe take a look at",
  something_else

Which I find easier than interpolation since you quite often have to split across lines anyways.

See {.fmt_msg_segment} for info about how each segment is formatted.

This methods joins the results together

@param [Array] segments

Message segments.

@return [String]

Formatted and joined message ready to pass up to the built-in
exception's `#initialize`.
# File lib/nrser/functions/text/format.rb, line 49
def self.fmt_msg *segments
  segments.map { |segment| fmt_msg_segment segment }.join( ' ' )
end
fmt_msg_segment(segment) click to toggle source

Format a segment of a message.

If `segment` responds to `#to_summary`, it will be called and the result will be returned.

Strings are simply returned. Other things are inspected (for now).

@param [Object] segment

The segment.

@return [String]

The formatted string for the segment.
# File lib/nrser/functions/text/format.rb, line 16
def self.fmt_msg_segment segment
  return segment.to_summary.to_s if segment.respond_to?( :to_summary )
  
  return segment if String === segment
  
  # TODO  Do better!
  segment.inspect
end
format_exception(e) click to toggle source

String format an exception the same way they are printed to the CLI when not handled (when they crash programs - what you're used to seeing), including the message, class and backtrace.

@param [Exception] e

Exception to format.

@return [String]

# File lib/nrser/functions/exception.rb, line 13
def self.format_exception e
  "#{ e.to_s } (#{ e.class }):\n  #{ e.backtrace.join("\n  ") }"
end
git_root(path = Pathname.getwd) click to toggle source

Get the absolute path to the root directory of the Git repo that `path` is in.

@note

In submodules, this will return the root of the submodule, **NOT**
of the top-level repo.

@param [String | Pathname] path

Path in Git repo that you want to find the root of.

Accepts relative and user (`~/...`) paths.

@return [Pathname]

# File lib/nrser/functions/git.rb, line 30
def self.git_root path = Pathname.getwd
  dir = dir_from path
  
  out, err, status = Open3.capture3 \
    'git rev-parse --show-toplevel',
    chdir: dir.to_s
  
  if status != 0
    message = \
      "#{ path.to_s.inspect } does not appear to be in a Git repo\n\n" +
      NRSER::Char::NULL.replace( err ) + "\n"
    
    raise SystemCallError.new message, status.exitstatus
  end
  
  Pathname.new out.chomp
end
guess_label_key_type(keyed) click to toggle source

Guess which type of “label” key - strings or symbols - a hash (or other object that responds to `#keys` and `#empty`) uses.

@param [#keys & empty] keyed

Hash or similar object that responds to `#keys` and `#empty` to guess
about.

@return [nil]

If we can't determine the type of "label" keys are used (there aren't
any or there is a mix).

@return [Class]

If we can determine that {String} or {Symbol} keys are exclusively
used returns that class.
# File lib/nrser/functions/hash/guess_label_key_type.rb, line 23
def self.guess_label_key_type keyed
  # We can't tell shit if the hash is empty
  return nil if keyed.empty?
  
  name_types = keyed.
    keys.
    map( &:class ).
    select { |klass| klass == String || klass == Symbol }.
    uniq
  
  return name_types[0] if name_types.length == 1
  
  # There are both string and symbol keys present, we can't guess
  nil
end
hash_like?(object) click to toggle source

Test if an object is “hash-like” - is it an Enumerable and does it respond to `#each_pair`?

@param [Object] object

Any old thing.

@return [Boolean]

`true` if `object` is "hash-like" for our purposes.
# File lib/nrser/functions/enumerable.rb, line 33
def self.hash_like? object
  object.is_a?( ::Enumerable ) &&
    object.respond_to?( :each_pair )
end
include_slice?(enum, slice, &is_match) click to toggle source

See if an `enum` includes a `slice`, using an optional block to do custom matching.

@example Order matters

NRSER.slice? [1, 2, 3], [2, 3]
# => true

NRSER.slice? [1, 2, 3], [3, 2]
# => false

@example The empty slice is always present

NRSER.slice? [1, 2, 3], []
# => true

NRSER.slice? [], []
# => true

@example Custom `&is_match` block to prefix-match

NRSER.slice?(
  ['neil', 'mica', 'hudie'],
  ['m', 'h']
) { |enum_entry, slice_entry|
  enum_entry.start_with? slice_entry
}
# => true

@note

Right now, just forwards to {NRSER.array_include_slice?}, which requires
that the {Enumerable}s support {Array}-like `#length` and `#slice`. I
took a swing at the general case but it came out messy and only partially
correct.

@param [Enumerable] enum

Sequence to search in.

@param [Enumerable] slice

Slice to search for.

@return [Boolean]

`true` if `enum` has a slice matching `slice`.
# File lib/nrser/functions/enumerable/include_slice.rb, line 49
  def self.include_slice? enum, slice, &is_match
    # Check that both args are {Enumerable}
    unless  Enumerable === enum &&
            Enumerable === slice
      raise TypeError.new binding.erb <<-END
        Both `enum` and `slice` must be {Enumerable}
        
        enum (<%= enum.class.safe_name %>):
        
            <%= enum.pretty_inspect %>
        
        slice (<%= slice.class.safe_name %>):
        
            <%= slice.pretty_inspect %>
        
      END
    end
    
    if [enum, slice].all? { |e|
      e.respond_to?( :length ) && e.respond_to?( :slice )
    }
      return array_include_slice? enum, slice, &is_match
    end
    
    raise NotImplementedError.new binding.erb <<-END
      Sorry, but general {Enumerable} slice include has not been implemented
      
      It's kinda complicated, or at least seems that way at first, so I'm
      going to punt for now...
    END
  end
indent(text, amount = 2, indent_string: nil, indent_empty_lines: false, skip_first_line: false) click to toggle source

adapted from active_support 4.2.0

<github.com/rails/rails/blob/7847a19f476fb9bee287681586d872ea43785e53/activesupport/lib/active_support/core_ext/string/indent.rb>

# File lib/nrser/functions/text/indentation.rb, line 31
def self.indent text,
                amount = 2,
                indent_string: nil,
                indent_empty_lines: false,
                skip_first_line: false
  if skip_first_line
    lines = self.lines text
    
    lines.first + indent(
      rest( lines ).join,
      amount,
      indent_string: indent_string,
      skip_first_line: false
    )
    
  else
    indent_string = indent_string || text[/^[ \t]/] || ' '
    re = indent_empty_lines ? /^/ : /^(?!$)/
    text.gsub re, indent_string * amount
    
  end
end
indent_tag(text, marker: INDENT_TAG_MARKER, separator: INDENT_TAG_SEPARATOR) click to toggle source

Tag each line of `text` with special marker characters around it's leading indent so that the resulting text string can be fed through an interpolation process like ERB that may inject multiline strings and the result can then be fed through {NRSER.indent_untag} to apply the correct indentation to the interpolated lines.

Each line of `text` is re-formatted like:

"<marker><leading_indent><separator><line_without_leading_indent>"

`marker` and `separator` can be configured via keyword arguments, but they

default to:
  • `marker` - {NRSER::INDENT_TAG_MARKER}, the no-printable ASCII *record separator* (ASCII character 30, “x1E” / “u001E”).

  • `separator` - {NRSER::INDENT_TAG_SEPARATOR}, the non-printable ASCII *unit separator* (ASCII character 31, “x1F” / “u001F”)

@example With default marker and separator

NRSER.indent_tag "    hey there!"
# => "\x1E    \x1Fhey there!"

@param [String] text

String text to indent tag.

@param [String] marker

Special string to mark the start of tagged lines. If interpolated text
lines start with this string you're going to have a bad time.

@param [String] separator

Special string to separate the leading indent from the rest of the line.

@return [String]

Tagged text.
# File lib/nrser/functions/text/indentation.rb, line 133
def self.indent_tag text,
                    marker: INDENT_TAG_MARKER,
                    separator: INDENT_TAG_SEPARATOR
  text.lines.map { |line|
    indent = if match = INDENT_RE.match( line )
      match[0]
    else
      ''
    end
    
    "#{ marker }#{ indent }#{ separator }#{ line[indent.length..-1] }"
  }.join
end
indent_untag(text, marker: INDENT_TAG_MARKER, separator: INDENT_TAG_SEPARATOR) click to toggle source

Reverse indent tagging that was done via {NRSER.indent_tag}, indenting any untagged lines to the same level as the one above them.

@param [String] text

Tagged text string.

@param [String] marker

Must be the marker used to tag the text.

@param [String] separator

Must be the separator used to tag the text.

@return [String]

Final text with interpolation and indent correction.
# File lib/nrser/functions/text/indentation.rb, line 163
def self.indent_untag text,
                      marker: INDENT_TAG_MARKER,
                      separator: INDENT_TAG_SEPARATOR
  
  current_indent = ''
  
  text.lines.map { |line|
    if line.start_with? marker
      current_indent, line = line[marker.length..-1].split( separator, 2 )
    end
    
    current_indent + line
    
  }.join
  
end
indented?(text) click to toggle source
# File lib/nrser/functions/text/indentation.rb, line 22
def self.indented? text
  !( find_indent( text ).empty? )
end
lazy_filter_repeated_blank_lines(source, remove_leading: false) click to toggle source
# File lib/nrser/functions/string.rb, line 79
def self.lazy_filter_repeated_blank_lines source, remove_leading: false
  skipping = remove_leading
  
  source = source.each_line if source.is_a? String
  
  Enumerator::Lazy.new source do |yielder, line|
    if line =~ /^\s*$/
      unless skipping
        yielder << line
      end
      skipping = true
    else
      skipping = false
      yielder << line
    end
  end
  
end
leaves(tree) click to toggle source

Create a new hash where all the values are the scalar “leaves” of the possibly nested `hash` param. Leaves are keyed by “key path” arrays representing the sequence of keys to dig that leaf out of the has param.

In abstract, if `h` is the `hash` param and

l = NRSER.leaves h

then for each key `k` and corresponding value `v` in `l`

h.dig( *k ) == v

@pure

Return value depends only on parameters.

@example Simple “flat” hash

NRSER.leaves( {a: 1, b: 2} )
=> {
  [:a] => 1,
  [:b] => 2,
}

@example Nested hash

NRSER.leaves(
  1 => {
    name: 'Neil',
    fav_color: 'blue',
  },
  2 => {
    name: 'Mica',
    fav_color: 'red',
  }
)
# => {
#   [1, :name]      => 'Neil',
#   [1, :fav_color] => 'blue',
#   [2, :name]      => 'Mica',
#   [2, :fav_color] => 'red',
# }

@param [#each_pair | (each_index & each_with_index)] tree

@return [Hash<Array, Object>]

# File lib/nrser/functions/tree/leaves.rb, line 51
def self.leaves tree
  {}.tap { |results|
    _internal_leaves tree, path: [], results: results
  }
end
lines(text) click to toggle source

Functions

# File lib/nrser/functions/text/lines.rb, line 40
def self.lines text
  case text
  when String
    text.lines
  when Array
    text
  else
    raise TypeError,
      "Expected String or Array, found #{ text.class.safe_name }"
  end
end
looks_globish?(path) click to toggle source
# File lib/nrser/functions/path.rb, line 64
def self.looks_globish? path
  %w|* ? [ {|.any? &path.to_s.method( :include? )
end
looks_like_json_array?(string) click to toggle source

Test if a string looks like it might encode an array in JSON format by seeing if it's first non-whitespace character is `[` and last non-whitespace character is `]`.

@param [String] string

String to test.

@return [Boolean]

`true` if we think `string` encodes a JSON array.
# File lib/nrser/functions/string/looks_like.rb, line 46
def self.looks_like_json_array? string
  !!( string =~ JSON_ARRAY_RE )
end
looks_like_json_object?(string) click to toggle source

Test if a string looks like it might encode an object in JSON format (JSON object becomes a {Hash} in Ruby) by seeing if it's first non-whitespace character is `{` and last non-whitespace character is `}`.

@param [String] string

String to test.

@return [Boolean]

`true` if we think `string` encodes a JSON object.
# File lib/nrser/functions/string/looks_like.rb, line 61
def self.looks_like_json_object? string
  !!( string =~ JSON_OBJECT_RE )
end
looks_like_yaml_object?(string) click to toggle source
# File lib/nrser/functions/string/looks_like.rb, line 66
def self.looks_like_yaml_object? string
  # YAML is (now) a super-set of JSON, so anything that looks like a JSON
  # object is kosh
  looks_like_json_object?( string ) || string.lines.all? { |line|
    line.start_with?( '---', '  ', '#' ) || line =~ /[^\ ].*\:/
  }
end
map(object, &block) click to toggle source

If `object` is a collection, calls `#map` with the block. Otherwise, applies block to the object and returns the result.

See note in {NRSER.each} for discussion of why this tests for a collection instead of duck-typing `#map`.

@param [Object] object

Target object.

@yield

Each element of a collection or the target object itself.

@return [Object]

The result of mapping or applying the block.
# File lib/nrser/collection.rb, line 89
def map object, &block
  if collection? object
    object.map &block
  else
    block.call object
  end
end
map_branches(tree, &block) click to toggle source

Map the immediate “branches” of a structure that can be used to compose our idea of a tree: nested hash-like and array-like structures like you would get from parsing a JSON document.

The `block` MUST return a pair ({Array} of length 2), the first value of which is the key or index in the new {Hash} or {Array}.

These pairs are then converted into a {Hash} or {Array} depending on it `tree` was {NRSER::Types.hash_like} or {NRSER::Types.array_like}, and that value is returned.

Uses {NRSER.each_branch} internally.

Written and tested against Hash and Array instances, but should work with anything:

  1. hash-like that responds to `#each_pair` appropriately.

  2. array-like that responds to `#each_index` and `#each_with_index` appropriately.

@pure

Return value depends only on parameters.

@note

Not sure what will happen if the tree has circular references!

@todo

Might be nice to have an option to preserve the tree class that creates
a new instance of *whatever* it was and populates that, though I could
see this relying on problematic assumptions and producing confusing
results depending on the actual classes.

Maybe this could be encoded in a mixin that we would detect or something.

@param [#each_pair | (each_index & each_with_index)] tree

Structure representing a tree via hash-like and array-like containers.

@yieldparam [Object] key

The first yielded param is the key or index for the value branch at the
top level of `tree`.

@yieldparam [Object] value

The second yielded param is the branch at the key or index at the top
level of `tree`.

@yieldreturn [Array]

Pair of key (/index) in new array or hash followed by value.

@return [Array | Hash]

If no block is provided.

@raise [TypeError | NoMethodError]

If `tree` does not respond to `#each_pair` or to `#each_index` and
`#each_with_index`.

@raise [ArgumentError]

If `&block` is not provided.
# File lib/nrser/functions/tree/map_branches.rb, line 64
  def self.map_branches tree, &block
    if block.nil?
      raise ArgumentError, "Must provide block"
    end
    
    pairs = each_branch( tree ).map &block
    
    if hash_like? tree
      pairs.to_h
    elsif array_like? tree
      pairs.each_with_object( [] ) { |(index, value), array|
        array[index] = value
      }
    else
      raise TypeError.new erb binding, <<-END
        Excepted `tree` arg to be array or hash-like.
        
        Received (<%= tree.class %>):
        
            <%= tree.pretty_inspect %>
        
      END
    end
    
  end
map_leaves(tree, &block) click to toggle source

@!group Tree Functions

# File lib/nrser/functions/tree/map_leaves.rb, line 5
def self.map_leaves tree, &block
  NRSER::Types.tree.check tree
  
  _internal_map_leaves tree, key_path: [], &block
end
map_tree(tree, prune: false, &block) click to toggle source

Recursively descend through a tree mapping all non-structural elements

  • anything not {NRSER::Types.hash_like} or {NRSER::Types.array_like}, both

hash keys and values, as well as array entries - through `block` to produce a new structure.

Useful when you want to translate pieces of a tree structure depending on their type or some other property that can be determined *from the element alone* - `block` receives only the value as an argument, no location information (because it's weirder to represent for keys and I didn't need it for the {NRSER.transformer} stuff this was written for).

@note

Array indexes **are not mapped** through `block` and can not be changed
via this method. This makes it easier to do things like "convert all the
integers to strings" when you mean the data entries, not the array
indexes (which would fail since the new array wouldn't accept string
indices).

If you don't want to map hash keys use {NRSER.map_leaves}.

See the specs for examples. Used in {NRSER.transformer}.

@param tree (see NRSER.each_branch)

@param [Boolean] prune

When `true`, prunes out values whose labels end with `?` and values are
`nil`.

@yieldparam [Object] element

Anything reached from the root that is not structural (hash-like or
array-like), including / inside hash keys (though array
indexes are **not** passed).
# File lib/nrser/functions/tree/map_tree.rb, line 35
def self.map_tree tree, prune: false, &block
  # TODO type check tree?
  
  mapped = tree.map { |element|
    # Recur if `element` is a tree.
    #
    # Since `element` will be an {Array} of `key`, `value` when `tree` is a
    # {Hash} (or similar), this will descend into hash keys that are also
    # trees, as well as into hash values and array entries.
    #
    if Types.tree.test element
      map_tree element, prune: prune, &block
    else
      # When we've run out of trees, finally pipe through the block:
      block.call element
    end
  }
  
  # If `tree` is hash-like, we want to convert the array of pair arrays
  # back into a hash.
  if Types.hash_like.test tree
    if prune
      pruned = {}
      
      mapped.each { |key, value|
        if  Types.Label.test( key ) &&
            key.to_s.end_with?( '?' )
          unless value.nil?
            new_key = key.to_s[0..-2]
            
            if key.is_a?( Symbol )
              new_key = new_key.to_sym
            end
            
            pruned[new_key] = value
          end
        else
          pruned[key] = value
        end
      }
      
      pruned
    else
      mapped.to_h
    end
  else
    # Getting here means it was array-like, so it's already fine
    mapped
  end
end
merge_by(current, *updates, &merge_key) click to toggle source

Deep merge arrays of data hashes, matching hashes by computing a key with `&merge_key`.

Uses {NRSER.deep_merge!} to merge.

@param [Array<Hash>] current

Current (base) array of hashes to start with (lowest predominance).

@param [Array<Hash>] updates

One or more arrays of update hashes to merge over `current` (last is
highest predominance).

@param [Proc<(Hash)=>Object>] merge_key

Each hash is passed to `&merge_key` and the result is used to match
hashes for merge. Must not return equal values for two different hashes
in any of the arrays (`current` or any of `*updates`).

@return [Array<Hash>]

Final array of merged hashes. Don't depend on order.
# File lib/nrser/functions/merge_by.rb, line 23
def self.merge_by current, *updates, &merge_key
  updates.reduce( assoc_by current, &merge_key ) { |result, update|
    result.deep_merge! assoc_by( update, &merge_key )
  }.values
end
message(*args, &block) click to toggle source

Creates a new {NRSER::Message} from the array.

@example

message = NRSER::Op.message( :fetch, :x )
message.send_to x: 'ex', y: 'why?'
# => 'ex'

@return [NRSER::Message]

# File lib/nrser/functions/proc.rb, line 13
def self.message *args, &block
  if args.length == 1 && args[0].is_a?( Message )
    args[0]
  else
    Message.new *args, &block
  end
end
method_objects_for(mod, include_super, type:, sort:, include_initialize: false) click to toggle source

Core private method that supports all the other “method getters”.

@private

@param [Module] mod

Module in question.

@param [Boolean] include_super

When `true`, includes inherited class methods.

@param [:class | :instance] type

Get class or instance methods.

@param [Boolean] sort

If `true`, will sort the methods by name, which is usually
the useful way to look at and use them.

@return [Array<(Method | UnboundMethod)>]

List of method objects (all bound to `mod`).
# File lib/nrser/functions/module/method_objects.rb, line 29
def self.method_objects_for mod,
                            include_super,
                            type:,
                            sort:,
                            include_initialize: false
  initialize_method = nil
  
  get_names, get_method = case type
  when :class
    [:methods, :method]
    
  when :instance
    if include_initialize
      # Only way I can figure out to find out if it is defined it to try
      # to get the object and handle the error
      begin
        initialize_method = mod.instance_method :initialize
      rescue NameError => error
      else
        # Don't want to include it if we're not `include_super` and it's
        # inherited from a different module
        unless include_super || initialize_method.owner == mod
          initialize_method = nil
        end
      end
    end
    
    [:instance_methods, :instance_method]
    
  else
    raise ArgumentError,
      "`type:` must be `:class` or `:instance`, found #{ type.inspect }"
    
  end # case type
  
  methods = mod.send( get_names, include_super ).map { |name|
    mod.send get_method, name
  }
  
  methods << initialize_method unless initialize_method.nil?
  
  methods.sort! { |a, b| a.name <=> b.name } if sort
  
  methods
end
normalized_path?(path) click to toggle source

Test if a path is what I'm calling “normalized” - generally free of any `.`, `..` or empty segments, with specific exceptions for `'/'` and `'.'`.

@param [String | Pathname] path

Path to test.

@return [Boolean]

`true` if we consider the path "normalized".

@raise [NRSER::TypeError]

If `path` is not a {String} or {Pathname}.
# File lib/nrser/functions/path/normalized.rb, line 37
def self.normalized_path? path
  string = case path
  when String
    path
  when Pathname
    path.to_s
  else
    raise NRSER::TypeError.new \
      "path must be String or Pathname, found", path,
      expected: [ String, Pathname ],
      found: path
  end

  # Examine each segment

  # NOTE  The `-1` is *extremely* important - it stops suppression of empty
  #       entries in the result, and we need them!
  segments = string.split File::SEPARATOR, -1 

  segments.
    # We need the indexes, since the first and last segments can be empty,
    # corresponding to `/...` and `.../` paths, respectively.
    each_with_index.
    # See if they all meet the requirements
    all? { |segment, index|
      (
        segment != '.' || # Can't have any `.../x/./y/...` business
        index == 0 # But we can have `./x/y/` and such
      ) &&
      segment != '..' && # Can't have any `.../x/../y/...` crap either
      (
        # and, finally, the segment can't be empty
        segment != '' ||
        # unless it's the first (`/x/...` case)
        index == 0 ||
        # or the last segment (`.../z/` case)
        index == segments.length - 1
      )
    }
end
only(enum, default: nil) click to toggle source

Return the first entry if the enumerable has `#count` one.

Otherwise, return `default` (which defaults to `nil`).

@param [Enumerable<E>] enum

Enumerable in question (really, anything that responds to `#first` and
`#count`).

@param [D] default

Value to return if `enum` does not have only one entry.

@return [E]

When `enum` has `#count == 1`.

@return [D]

When `enum` does not have `#count == 1`.
# File lib/nrser/functions/enumerable.rb, line 121
def self.only enum, default: nil
  if enum.count == 1
    enum.first
  else
    default
  end
end
only!(enum) click to toggle source

Return the only entry if the enumerable has `#count` one. Otherwise raise an error.

@param enum (see NRSER.only)

@return [E]

First element of `enum`.

@raise [ArgumentError]

If `enum` does not have `#count == 1`.
# File lib/nrser/functions/enumerable.rb, line 141
def self.only! enum
  count = enum.count
  
  unless count == 1
    raise NRSER::CountError.new value: enum,
                                count: count,
                                expected: 1
  end
  
  enum.first
end
pn_from(path) click to toggle source

@return [Pathname]

# File lib/nrser/functions/path.rb, line 32
def self.pn_from path
  if path.is_a? Pathname
    path
  else
    Pathname.new path
  end
end
private_sender(symbol, *args, &block) click to toggle source

Create a {Proc} that sends the arguments to a receiver via `#send`, forcing access to private and protected methods.

Equivalent to

message( symbol, *args, &block ).to_proc publicly: false

Pretty much here for completeness' sake.

@example

sender( :fetch, :x ).call x: 'ex'
# => 'ex'

@return [Proc]

# File lib/nrser/functions/proc.rb, line 62
def self.private_sender symbol, *args, &block
  message( symbol, *args, &block ).to_proc publicly: false
end
public_sender(symbol, *args, &block) click to toggle source

Create a {Proc} that sends the arguments to a receiver via `#public_send`.

Equivalent to

message( symbol, *args, &block ).to_proc

Pretty much here for completeness' sake.

@example

sender( :fetch, :x ).call x: 'ex'
# => 'ex'

@return [Proc]

# File lib/nrser/functions/proc.rb, line 39
def self.public_sender symbol, *args, &block
  message( symbol, *args, &block ).to_proc
end
rest(array) click to toggle source

Functional implementation of “rest” for arrays. Used when refining `#rest` into {Array}.

@param [Array] array

@return [return_type]

New array consisting of all elements after the first.
# File lib/nrser/functions/array.rb, line 11
def self.rest array
  array[1..-1]
end
retriever(key) click to toggle source

Return a {Proc} that accepts a single argument that must respond to `#[]` and retrieves `key` from it.

@param [String | Symbol | Integer] key

Key (or index) to retrieve.

@return [Proc]

# File lib/nrser/functions/proc.rb, line 105
def self.retriever key
  ->( indexed ) { indexed[key] }
end
smart_ellipsis(string, max, omission: UNICODE_ELLIPSIS, split: ', ') click to toggle source

Try to do “smart” job adding ellipsis to the middle of strings by splitting them by a separator `split` - that defaults to `, ` - then building the result up by bouncing back and forth between tokens at the beginning and end of the string until we reach the `max` length limit.

Intended to be used with possibly long single-line strings like `#inspect` returns for complex objects, where tokens are commonly separated by `, `, and producing a reasonably nice result that will fit in a reasonable amount of space, like `rspec` output (which was the motivation).

If `string` is already less than `max` then it is just returned.

If `string` doesn't contain `split` or just the first and last tokens alone would push the result over `max` then falls back to {NRSER.ellipsis}.

If `max` is too small it's going to fall back nearly always… around `64` has seemed like a decent place to start from screwing around on the REPL a bit.

@pure

Return value depends only on parameters.

@status

Experimental

@param [String] string

Source string.

@param [Fixnum] max

Max length to allow for the output string. Result will usually be
*less* than this unless the fallback to {NRSER.ellipsis} kicks in.

@param [String] omission

The string to stick in the middle where original contents were
removed. Defaults to the unicode ellipsis since I'm targeting the CLI
at the moment and it saves precious characters.

@param [String] split

The string to tokenize the `string` parameter by. If you pass a
{Regexp} here it might work, it might loop out, maybe.

@return [String]

String of at most `max` length with the middle chopped out if needed
to do so.
# File lib/nrser/functions/string.rb, line 191
  def self.smart_ellipsis string, max, omission: UNICODE_ELLIPSIS, split: ', '
    return string unless string.length > max
    
    unless string.include? split
      return ellipsis string, max, omission: omission
    end
    
    tokens = string.split split
    
    char_budget = max - omission.length
    start = tokens[0] + split
    finish = tokens[tokens.length - 1]
    
    if start.length + finish.length > char_budget
      return ellipsis string, max, omission: omission
    end
    
    next_start_index = 1
    next_finish_index = tokens.length - 2
    next_index_is = :start
    next_index = next_start_index
    
    while (
      start.length +
      finish.length +
      tokens[next_index].length +
      split.length
    ) <= char_budget do
      if next_index_is == :start
        start += tokens[next_index] + split
        next_start_index += 1
        next_index = next_finish_index
        next_index_is = :finish
      else # == :finish
        finish = tokens[next_index] + split + finish
        next_finish_index -= 1
        next_index = next_start_index
        next_index_is = :start
      end
    end
    
    start + omission + finish
    
  end # .smart_ellipsis
  
  # @!endgroup String Functions
  
end
squish(str) click to toggle source

turn a multi-line string into a single line, collapsing whitespace to a single space.

same as ActiveSupport's String.squish, adapted from there.

# File lib/nrser/functions/string.rb, line 37
def self.squish str
  str.gsub(/[[:space:]]+/, ' ').strip
end
to_open_struct(hash, freeze: false) click to toggle source

Deeply convert a {Hash} to an {OpenStruct}.

@param [Hash] hash

@return [OpenStruct]

@raise [TypeError]

If `hash` is not a {Hash}.
# File lib/nrser/functions/open_struct.rb, line 12
def self.to_open_struct hash, freeze: false
  unless hash.is_a? Hash
    raise TypeError,
          "Argument must be hash (found #{ hash.inspect })"
  end
  
  _to_open_struct hash, freeze: freeze
end
transform(tree, source) click to toggle source
# File lib/nrser/functions/tree/transform.rb, line 4
def self.transform tree, source
  map_tree( tree, prune: true ) { |value|
    if value.is_a? Proc
      value.call source
    else
      value
    end
  }
end
transformer(&block) click to toggle source
# File lib/nrser/functions/tree/transform.rb, line 35
def self.transformer &block
  map_tree( block.call SendSerializer.new ) { |value|
    if value.is_a? SendSerializer
      value.to_proc
    else
      value
    end
  }
end
truthy?(object) click to toggle source

Evaluate an object (that probably came from outside Ruby, like an environment variable) to see if it's meant to represent true or false.

@pure Return value depends only on parameters.

@param [nil | String | Boolean] object

Value to test.

@return [Boolean]

`true` if the object is "truthy".

@raise [ArgumentError]

When a string is received that is not in {NRSER::TRUTHY_STRINGS} or
{NRSER::FALSY_STRINGS} (case insensitive).

@raise [TypeError]

When `object` is not the right type.
# File lib/nrser/functions/object/truthy.rb, line 64
def self.truthy? object
  case object
  when nil
    false
    
  when String
    downcased = object.downcase
    
    if TRUTHY_STRINGS.include? downcased
      true
    elsif FALSY_STRINGS.include? downcased
      false
    else
      raise ArgumentError,
            "String #{ object.inspect } not recognized as true or false."
    end
    
  when TrueClass, FalseClass
    object
    
  else
    raise TypeError,
          "Can't evaluate truthiness of #{ object.inspect }"
  end
end
try_find(enum, &block) click to toggle source

Like `Enumerable#find`, but wraps each call to `&block` in a `begin` / `rescue`, returning the result of the first call that doesn't raise an error.

If no calls succeed, raises a {NRSER::MultipleErrors} containing the errors from the block calls.

@param [Enumerable<E>] enum

Values to call `&block` with.

@param [Proc<E=>V>] block

Block to call, which is expected to raise an error if it fails.

@return [V]

Result of first call to `&block` that doesn't raise.

@raise [ArgumentError]

If `enum` was empty (`enum#each` never yielded).

@raise [NRSER::MultipleErrors]

If all calls to `&block` failed.
# File lib/nrser/functions/enumerable.rb, line 232
def self.try_find enum, &block
  errors = []
  
  enum.each do |*args|
    begin
      result = block.call *args
    rescue Exception => error
      errors << error
    else
      return result
    end
  end
  
  if errors.empty?
    raise ArgumentError,
      "Appears that enumerable was empty: #{ enum.inspect }"
  else
    raise NRSER::MultipleErrors.new errors
  end
end
u_bold(string) click to toggle source

Proxies to {NRSER::Char::AlphaNumericSub#sub} on {NRSER::Char::AlphaNumericSub.unicode_math_italic} to convert regular UTF-8/ASCII `a-zA-Z` characters to the “Unicode Math Italic” set.

@param [String] string

Input.

@return [String]

Output. Probably won't be `#ascii_only?`.
# File lib/nrser/functions/string/style.rb, line 37
def self.u_bold string
  NRSER::Char::AlphaNumericSub.unicode_math_bold.sub string
end
u_bold_italic(string) click to toggle source

Proxies to {NRSER::Char::AlphaNumericSub#sub} on {NRSER::Char::AlphaNumericSub.unicode_math_bold_italic} to convert regular UTF-8/ASCII `a-zA-Z` characters to the “Unicode Math Bold Italic” set.

@param [String] string

Input.

@return [String]

Output. Probably won't be `#ascii_only?`.
# File lib/nrser/functions/string/style.rb, line 52
def self.u_bold_italic string
  NRSER::Char::AlphaNumericSub.unicode_math_bold_italic.sub string
end
u_italic(string) click to toggle source

Proxies to {NRSER::Char::AlphaNumericSub#sub} on {NRSER::Char::AlphaNumericSub.unicode_math_italic} to convert regular UTF-8/ASCII `a-zA-Z` characters to the “Unicode Math Italic” set.

@param [String] string

Input.

@return [String]

Output. Probably won't be `#ascii_only?`.
# File lib/nrser/functions/string/style.rb, line 22
def self.u_italic string
  NRSER::Char::AlphaNumericSub.unicode_math_italic.sub string
end
u_mono(string) click to toggle source

Proxies to {NRSER::Char::AlphaNumericSub#sub} on {NRSER::Char::AlphaNumericSub.unicode_math_monospace} to convert regular UTF-8/ASCII `a-zA-Z` characters to the “Unicode Math Monospace” set.

@param [String] string

Input.

@return [String]

Output. Probably won't be `#ascii_only?`.
# File lib/nrser/functions/string/style.rb, line 67
def self.u_mono string
  NRSER::Char::AlphaNumericSub.unicode_math_monospace.sub string
end
whitespace?(string) click to toggle source

@!group String Functions

# File lib/nrser/functions/string.rb, line 28
def self.whitespace? string
  string =~ WHITESPACE_RE
end
with_indent_tagged(text, marker: INDENT_TAG_MARKER, separator: INDENT_TAG_SEPARATOR, &interpolate_block) click to toggle source

Indent tag a some text via {NRSER.indent_tag}, call the block with it, then pass the result through {NRSER.indent_untag} and return that.

@param [String] marker

Special string to mark the start of tagged lines. If interpolated text
lines start with this string you're going to have a bad time.

@param [String] separator

Must be the separator used to tag the text.

@return [String]

Final text with interpolation and indent correction.
# File lib/nrser/functions/text/indentation.rb, line 195
def self.with_indent_tagged text,
                            marker: INDENT_TAG_MARKER,
                            separator: INDENT_TAG_SEPARATOR,
                            &interpolate_block
  indent_untag(
    interpolate_block.call(
      indent_tag text, marker: marker, separator: separator
    ),
    marker: marker,
    separator: separator,
  )
end
word_wrap(text, line_width: 80, break_sequence: "\n") click to toggle source

Split text at whitespace to fit in line length. Lifted from Rails' ActionView.

@see api.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-word_wrap

@param [String] text

Text to word wrap.

@param [Fixnum] line_width

Line with in number of character to wrap at.

@param [String] break_sequence

String to join lines with.

@return [String]

@todo Document return value.
# File lib/nrser/functions/text/word_wrap.rb, line 21
def self.word_wrap text, line_width: 80, break_sequence: "\n"
  text.split("\n").collect! do |line|
    line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line
  end * break_sequence
end
words(string) click to toggle source

Split a string into 'words' for word-based matching.

@param [String] string

Input string.

@return [Array<String>]

Array of non-empty words in `string`.
# File lib/nrser/functions/text/words.rb, line 19
def self.words string
  string.split(/[\W_\-\/]+/).reject { |w| w.empty? }
end

Private Class Methods

_internal_bury!(tree, key_path, value, guess_key_type:, clobber:, create_arrays_for_unsigned_keys: key, *rest = key_path) click to toggle source
# File lib/nrser/functions/hash/bury.rb, line 72
  def self._internal_bury! tree,
                      key_path,
                      value,
                      guess_key_type:,
                      clobber:,
                      create_arrays_for_unsigned_keys:
                      
    # Split the key path into the current key and the rest of the keys
    key, *rest = key_path
    
    # If we are
    #
    # -   Guessing the key type
    # -   The tree is keyed
    # -   The tree uses some {Symbol} (and no {String}) keys
    #
    # then convert the key to a symbol.
    #
    if  guess_key_type &&
        tree.respond_to?( :keys ) &&
        guess_label_key_type( tree ) == Symbol
      key = key.to_sym
    end
    
    # Terminating case: we're at the last segment
    if rest.empty?
      # Set the value
      tree[key] = value
      
    else
      # Go deeper...
      
      # See if there is a hash in place
      unless NRSER::Types.tree.test tree[key]
        # There is not... so we need to do some figurin'
        
        # If we're clobbering or the hash has no value, we're good:
        # assign a new hash to set in
        if clobber || tree[key].nil?
          if  create_arrays_for_unsigned_keys &&
              NRSER::Types.unsigned.test( key )
            tree[key] = []
          else
            tree[key] = {}
          end
          
        else
          # We've got an intractable state conflict; raise
          raise NRSER::ConflictError.new squish <<-END
            can not set key #{ key.inspect } due to conflicting value
            #{ tree[key].inspect } in tree #{ tree.inspect } (:clobber
            option not set)
          END
          
        end
      end # unless hash[key].is_a?( Hash )
      
      # Dive in...
      bury! tree[key], rest, value
      
    end # if rest.empty? / else
  end
_internal_leaves(tree, path:, results: NRSER.each_branch( tree ) { |key, value| new_path = [*path, key]) click to toggle source

Internal recursive implementation for {NRSER.leaves}.

@private

@pure

Return value depends only on parameters.

@param [#each_pair | (each_index & each_with_index)] tree

Tree to walk.

@param [Array] path

Key path down to `tree`.

@param [Hash<Array, Object>] results

New hash to stick results in.

@return [nil]

# File lib/nrser/functions/tree/leaves.rb, line 76
def self._internal_leaves tree, path:, results:
  NRSER.each_branch( tree ) { |key, value|
    new_path = [*path, key]
    
    if NRSER::Types.tree.test value
      _internal_leaves value, path: new_path, results: results
    else
      results[new_path] = value
    end
  }
  
  nil
end
_internal_map_leaves(tree, key_path:, &block) click to toggle source

Internal recursive implementation for {NRSER.leaves}.

@param [#each_pair | (each_index & each_with_index)] tree

Tree to walk.

@param [Array] key_path

Key path down to `tree`.

@param [Proc] block

Called with each `(key_path, value)` pair.

@return [nil]

# File lib/nrser/functions/tree/map_leaves.rb, line 25
def self._internal_map_leaves tree, key_path:, &block
  NRSER::Types.match tree,
    NRSER::Types.hash_like, ->( hash_like ) {
      hash_like.map { |key, value|
        new_key_path = [*key_path, key]
        
        new_value = if NRSER::Types.tree.test( value )
          _internal_map_leaves value, key_path: new_key_path, &block
        else
          block.call new_key_path, value
        end
        
        [key, new_value]
      }.to_h
    },
    
    NRSER::Types.array_like, ->( array_like ) {
      array_like.each_with_index.map { |value, index|
        new_key_path = [*key_path, index]
        
        if NRSER::Types.tree.test( value )
          _internal_map_leaves value, key_path: new_key_path, &block
        else
          block.call new_key_path, value
        end
      }
    }
end