Module: NRSER

Defined in:
lib/nrser.rb,
lib/nrser/rspex.rb,
lib/nrser/errors.rb,
lib/nrser/logger.rb,
lib/nrser/no_arg.rb,
lib/nrser/message.rb,
lib/nrser/version.rb,
lib/nrser/collection.rb,
lib/nrser/temp/where.rb,
lib/nrser/types/array.rb,
lib/nrser/types/pairs.rb,
lib/nrser/types/paths.rb,
lib/nrser/types/labels.rb,
lib/nrser/functions/path.rb,
lib/nrser/functions/proc.rb,
lib/nrser/functions/array.rb,
lib/nrser/meta/props/prop.rb,
lib/nrser/refinements/set.rb,
lib/nrser/functions/string.rb,
lib/nrser/refinements/hash.rb,
lib/nrser/functions/binding.rb,
lib/nrser/refinements/array.rb,
lib/nrser/refinements/types.rb,
lib/nrser/temp/unicode_math.rb,
lib/nrser/functions/merge_by.rb,
lib/nrser/refinements/object.rb,
lib/nrser/refinements/string.rb,
lib/nrser/refinements/symbol.rb,
lib/nrser/functions/exception.rb,
lib/nrser/functions/hash/bury.rb,
lib/nrser/refinements/binding.rb,
lib/nrser/functions/enumerable.rb,
lib/nrser/functions/text/lines.rb,
lib/nrser/refinements/pathname.rb,
lib/nrser/functions/open_struct.rb,
lib/nrser/functions/tree/leaves.rb,
lib/nrser/refinements/exception.rb,
lib/nrser/rspex/shared_examples.rb,
lib/nrser/refinements/enumerator.rb,
lib/nrser/functions/object/truthy.rb,
lib/nrser/functions/tree/map_tree.rb,
lib/nrser/refinements/open_struct.rb,
lib/nrser/functions/object/as_hash.rb,
lib/nrser/functions/text/word_wrap.rb,
lib/nrser/functions/tree/transform.rb,
lib/nrser/functions/hash/deep_merge.rb,
lib/nrser/functions/hash/slice_keys.rb,
lib/nrser/functions/object/as_array.rb,
lib/nrser/functions/tree/map_leaves.rb,
lib/nrser/functions/hash/except_keys.rb,
lib/nrser/functions/text/indentation.rb,
lib/nrser/functions/tree/each_branch.rb,
lib/nrser/functions/string/looks_like.rb,
lib/nrser/functions/tree/map_branches.rb,
lib/nrser/functions/enumerable/find_map.rb,
lib/nrser/functions/hash/stringify_keys.rb,
lib/nrser/functions/hash/symbolize_keys.rb,
lib/nrser/functions/hash/transform_keys.rb,
lib/nrser/functions/enumerable/find_all_map.rb,
lib/nrser/functions/hash/guess_label_key_type.rb

Overview

Definitions

Defined Under Namespace

Modules: Collection, Ext, Meta, RSpex, Refinements, Types, UnicodeMath, Version Classes: AbstractMethodError, ConflictError, Lines, Logger, Message, MultipleErrors, NoArg, SendSerializer, Where

String Functions collapse

WHITESPACE_RE =
/\A[[:space:]]*\z/
UNICODE_ELLIPSIS =
''
JSON_ARRAY_RE =

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

Returns:

  • (Regexp)
/\A\s*\[.*\]\s*\z/m.freeze

Text Functions collapse

INDENT_RE =

Constants

/\A[\ \t]*/
INDENT_TAG_MARKER =
"\x1E"
INDENT_TAG_SEPARATOR =
"\x1F"

Object Functions collapse

TRUTHY_STRINGS =

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

Returns:

  • (Set<String>)
Set.new [
  'true',
  't',
  'yes',
  'y',
  'on',
  '1',
].freeze
FALSY_STRINGS =

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

Returns:

  • (Set<String>)
Set.new [
  'false',
  'f',
  'no',
  'n',
  'off',
  '0',
  '',
].freeze

Constant Summary collapse

ROOT =

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

Returns:

  • (Pathname)
( Pathname.new(__FILE__).dirname / '..' ).expand_path
NO_ARG =
NoArg.instance
VERSION =
"0.1.0"

Path Functions collapse

String Functions collapse

Exception Functions collapse

Hash Functions collapse

Enumerable Functions collapse

Text Functions collapse

Tree Functions collapse

Object Functions collapse

Class Method Summary collapse

Class Method Details

.array_like?(object) ⇒ Boolean

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

Parameters:

  • object (Object)

    Any old thing.

Returns:

  • (Boolean)

    ‘true` if `object` is “array-like” for our purposes.



17
18
19
20
# File 'lib/nrser/functions/enumerable.rb', line 17

def self.array_like? object
  object.is_a?( ::Enumerable ) &&
    object.respond_to?( :each_index )
end

.as_array(value) ⇒ Array

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`.

Parameters:

  • value (Object)

Returns:

  • (Array)


25
26
27
28
29
30
31
32
33
34
35
36
37
# 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) ⇒ Hash

TODO:

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

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 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 `’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.

Parameters:

  • value (Object)

    The value that we want to be a hash.

  • key (Object) (defaults to: nil)
    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.

Returns:

  • (Hash)

Raises:

  • (ArgumentError)

    If it comes to constructing a new Hash with ‘value` as a value and no argument was provided



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# 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

.bury!(hash, key_path, value, parsed_key_type: :guess, clobber: false, create_arrays_for_unsigned_keys: false) ⇒ return_type

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.

Parameters:

  • hash (Hash)

    Hash to bury the value in.

  • key_path (Array | #to_s)
    • 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.

  • value (Object)

    The value to set at the end of the path.

  • parsed_key_type: (Class | :guess) (defaults to: :guess)

    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) -

Returns:

  • (return_type)

    @todo Document return value.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 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) ⇒ Proc

Note:

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

Map *each entry* in ‘mappable` to a 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.

Examples:

Equivalent of ‘Time.now.to_i`


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

Returns:

  • (Proc)


87
88
89
90
91
92
93
94
95
# File 'lib/nrser/functions/proc.rb', line 87

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) ⇒ Boolean

test if an object is considered a collection.

Parameters:

  • obj (Object)

    object to test

Returns:

  • (Boolean)

    true if ‘obj` is a collection.



25
26
27
# 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) ⇒ Object

Raises:

  • (ArgumentError)


30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/nrser/functions/string.rb', line 30

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

.constantize(camel_cased_word) ⇒ Object

Get the constant identified by a string.

Lifted from ActiveSupport.

Examples:


SomeClass == NRSER.constantize(SomeClass.name)

Parameters:

  • camel_cased_word (String)

    The constant’s camel-cased, double-colon-separated “name”, like “NRSER::Types::Array”.

Returns:

  • (Object)

Raises:

  • (NameError)

    When the name is not in CamelCase or is not initialized.



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/nrser/functions/string.rb', line 264

def self.constantize(camel_cased_word)
  names = camel_cased_word.split('::')

  # Trigger a built-in NameError exception including the ill-formed constant in the message.
  Object.const_get(camel_cased_word) if names.empty?

  # Remove the first blank element in case of '::ClassName' notation.
  names.shift if names.size > 1 && names.first.empty?

  names.inject(Object) do |constant, name|
    if constant == Object
      constant.const_get(name)
    else
      candidate = constant.const_get(name)
      next candidate if constant.const_defined?(name, false)
      next candidate unless Object.const_defined?(name)

      # Go down the ancestors to check if it is owned directly. The check
      # stops when we reach Object or the end of ancestors tree.
      constant = constant.ancestors.inject do |const, ancestor|
        break const    if ancestor == Object
        break ancestor if ancestor.const_defined?(name, false)
        const
      end

      # owner is in Object, so raise
      constant.const_get(name, false)
    end
  end
end

.count_by(enum, &block) ⇒ Hash{C=>Integer}

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

Examples:

Count array entries by class


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

Parameters:

  • enum (Enumerable<E>)

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

  • &block (Proc<(E)=>C>)

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

Returns:

  • (Hash{C=>Integer})

    Hash mapping groups to positive integer counts.



294
295
296
297
298
# File 'lib/nrser/functions/enumerable.rb', line 294

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) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/nrser/functions/text/indentation.rb', line 55

def self.dedent text, ignore_whitespace_lines: true
  return text if text.empty?
  
  all_lines = text.lines
  
  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?
  
  all_lines.map { |line|
    if line.start_with? indent
      line[indent.length..-1]
    elsif line.end_with? "\n"
      "\n"
    else
      ""
    end
  }.join
end

.deep_merge(base_hash, other_hash, &block) ⇒ Hash

Returns a new hash created by recursively merging ‘other_hash` on top of `base_hash`.

Adapted from ActiveSupport.

Parameters:

  • base_hash (Hash)

    Base hash - it’s values will be overwritten by any key paths shared with the other hash.

  • other_hash (Hash)

    “Update” hash - it’s values will overwrite values at the same key path in the base hash.

    I don’t love the name; just went with what ActiveSupport used.

Returns:

  • (Hash)

    New merged hash.

See Also:



28
29
30
# File 'lib/nrser/functions/hash/deep_merge.rb', line 28

def self.deep_merge base_hash, other_hash, &block
  deep_merge! base_hash.dup, other_hash, &block
end

.deep_merge!(base_hash, other_hash, &block) ⇒ Hash

Same as deep_merge, but modifies ‘base_hash`.

Returns:

  • (Hash)

    The mutated base hash.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/nrser/functions/hash/deep_merge.rb', line 38

def self.deep_merge! base_hash, other_hash, &block
  other_hash.each_pair do |current_key, other_value|
    this_value = base_hash[current_key]

    base_hash[current_key] = if this_value.is_a?(Hash) &&
                                other_value.is_a?(Hash)
      deep_merge this_value, other_value, &block
    else
      if block_given? && base_hash.key?( current_key )
        block.call(current_key, this_value, other_value)
      else
        other_value
      end
    end
  end

  base_hash
end

.deep_stringify_keys(object) ⇒ return_type

TODO:

Document deep_stringify_keys method.

Returns @todo Document return value.

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



48
49
50
# File 'lib/nrser/functions/hash/stringify_keys.rb', line 48

def self.deep_stringify_keys object
  deep_transform_keys object, &:to_s
end

.deep_symbolize_keys(object, &block) ⇒ return_type

TODO:

Document deep_symbolize_keys method.

Returns @todo Document return value.

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



52
53
54
# File 'lib/nrser/functions/hash/symbolize_keys.rb', line 52

def self.deep_symbolize_keys object, &block
  deep_transform_keys( object ) { key.to_sym rescue key }
end

.deep_transform_keys(object, &block) ⇒ Object

TODO:

Maybe this is a tree function?

Deeply transform Hash keys that we can find by traversing Hash and Array instances that we can find from ‘object` and piping keys through `block`.

From ActiveSupport.

Examples:

Hash top node


NRSER.deep_transform_keys({
  people: {
    jane: {fav_color: 'red'},
    joe:  {fav_color: 'blue'},
  }
}) { |key| key.to_s.upcase }
# => {
# "PEOPLE" => {
#   "JANE" => {"FAV_COLOR" => 'red'},
#   "JOE"  => {"FAV_COLOR" => 'blue'},
# }

Array top node


NRSER.deep_transform_keys(
  [{x: 2}, {y: 'blue'}, 3]
) { |key| key.to_s.upcase }
# => [{'X' => 2}, {'Y' => 'blue'}, 3]

Non-array or hash value


NRSER.deep_transform_keys 'blah'
# => 'blah'

Parameters:

  • object (Object)

    Anything; see examples.

  • &block (Proc)

    Proc that should accept each key as it’s only argument and return the new key to replace it with.

See Also:



92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/nrser/functions/hash/transform_keys.rb', line 92

def self.deep_transform_keys object, &block
  case object
  when Hash
    object.each_with_object( {} ) do |(key, value), result|
      result[block.call( key )] = deep_transform_keys value, &block
    end
  when Array
    object.map { |entry| deep_transform_keys entry, &block }
  else
    object
  end
end

.deep_transform_keys!(object, &block) ⇒ Object

Like deep_transform_keys but mutates the objects (works in place).

Parameters:

  • object (Object)

    Anything; see examples.

  • &block (Proc)

    Proc that should accept each key as it’s only argument and return the new key to replace it with.

Returns:

  • (Object)

    The ‘object` that was passed in, post mutations.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/nrser/functions/hash/transform_keys.rb', line 119

def self.deep_transform_keys! object, &block
  case object
  when Hash
    object.keys.each do |key|
      value = object.delete key
      object[block.call( key )] = deep_transform_keys! value, &block
    end
    object
  when Array
    object.map! {|e| deep_transform_keys!(e, &block)}
  else
    object
  end
end

.each(object) { ... } ⇒ Object

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}").

Parameters:

  • object (Object)

    Target object.

Yields:

  • Each element of a collection or the target object itself.

Returns:

  • (Object)

    ‘object` param.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# 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) {|key, value| ... } ⇒ Enumerator, #each_pair | (#each_index & #each_with_index)

Note:

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

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`.

Parameters:

  • tree (#each_pair | (#each_index & #each_with_index))

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

Yield Parameters:

  • key (Object)

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

  • value (Object)

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

Yield Returns:

  • Ignored.

Returns:

  • (Enumerator)

    If no block is provided.

  • (#each_pair | (#each_index & #each_with_index))

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

Raises:

  • (NoMethodError)

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



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# 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(string, max, omission: UNICODE_ELLIPSIS) ⇒ String

Cut the middle out of a string and stick an ellipsis in there instead.

Parameters:

  • string (String)

    Source string.

  • max (Fixnum)

    Max length to allow for the output string.

  • omission: (String) (defaults to: UNICODE_ELLIPSIS)

    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.

Returns:

  • (String)

    String of at most ‘max` length with the middle chopped out if needed to do so.



140
141
142
143
144
145
146
147
148
149
# File 'lib/nrser/functions/string.rb', line 140

def self.ellipsis string, max, omission: UNICODE_ELLIPSIS
  return string unless string.length > max
  
  trim_to = max - omission.length
  
  start = string[0, (trim_to / 2) + (trim_to % 2)]
  finish = string[-( (trim_to / 2) - (trim_to % 2) )..-1]
  
  start + omission + finish
end

.enumerate_as_values(enum) ⇒ Enumerator

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`.

Parameters:

  • enum (Enumerable)

Returns:

  • (Enumerator)

Raises:

  • (ArgumentError)

    If ‘enum` doesn’t respond to ‘#each_value` or `#each_entry`.



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/nrser/functions/enumerable.rb', line 256

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

.erb(bnd, str) ⇒ Object Also known as: template



4
5
6
7
8
9
10
11
12
13
# File 'lib/nrser/functions/binding.rb', line 4

def erb bnd, str
  require 'erb'
  
  filter_repeated_blank_lines(
    with_indent_tagged( dedent( str ) ) { |tagged_str|
      ERB.new( tagged_str ).result( bnd )
    },
    remove_leading: true
  )
end

.except_keys(hash, *keys) ⇒ Hash

Returns a new hash without ‘keys`.

Lifted from ActiveSupport.

Parameters:

  • hash (Hash)

    Source hash.

Returns:

  • (Hash)

See Also:



38
39
40
# File 'lib/nrser/functions/hash/except_keys.rb', line 38

def self.except_keys hash, *keys
  except_keys! hash.dup, *keys
end

.except_keys!(hash, *keys) ⇒ Hash

Removes the given keys from hash and returns it.

Lifted from ActiveSupport.

Parameters:

  • hash (Hash)

    Hash to mutate.

Returns:

  • (Hash)

See Also:



19
20
21
22
# File 'lib/nrser/functions/hash/except_keys.rb', line 19

def self.except_keys! hash, *keys
  keys.each { |key| hash.delete(key) }
  hash
end

.extract_from_array!(array, &block) ⇒ Object

A destructive partition.



17
18
19
20
21
22
23
24
25
26
27
# 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) ⇒ Boolean

Opposite of truthy?.

Parameters:

  • object (nil | String | Boolean)

    Value to test.

Returns:

  • (Boolean)

    The negation of truthy?.

Raises:

  • (ArgumentError)

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

  • (TypeError)

    When ‘object` is not the right type.



107
108
109
# File 'lib/nrser/functions/object/truthy.rb', line 107

def self.falsy? object
  ! truthy?(object)
end

.filter_repeated_blank_lines(str, remove_leading: false) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/nrser/functions/string.rb', line 46

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) ⇒ nil, R

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

Examples:


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

Parameters:

  • enum (Enumerable<E>)

    Entries to search (in order).

  • &block (Proc<(E)=>R>)

    Block mapping entires to results.

Returns:

  • (nil)

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

  • (R)

    The first result ‘R = block.call( E )` where `R` is not `nil` or `false`.



29
30
31
# 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) ⇒ Array<E>

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.

Parameters:

  • enum (Enumerable<E>)

    The entries to search and check.

  • bounds (Integer | Hash)

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

  • &block (Proc)

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

Returns:

  • (Array<E>)

    Found entries from ‘enum`.

Raises:

  • (TypeError)

    If the results of ‘enum.find_all &block` don’t satisfy ‘bounds`.



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/nrser/functions/enumerable.rb', line 115

def self.find_bounded enum, bounds, &block
  NRSER::Types.
    length(bounds).
    check(enum.find_all &block) { |type:, value:|
      erb binding, <<-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) ⇒ Object

Functions



17
18
19
# 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_map(enum, ifnone = nil, &block) ⇒ nil, ...

Find the first truthy (not ‘nil` or `false`) result of calling `&block` with entries from `enum`.

Like Enumerable#find, accept an optional ‘ifnone` procedure to call if no match is found.

Examples:


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

Parameters:

  • enum (Enumerable<E>)

    Entries to search (in order).

  • ifnone (nil | Proc<()=>DEFAULT>) (defaults to: nil)

    Optional lambda to call for the return value when no match is found.

  • &block (Proc<(E)=>RESLUT>)

    Block mapping entires to results.

Returns:

  • (nil)

    When ‘block.call( E )` is `nil` or `false` for all `E` in `enum` and `ifnone` is `nil` or not provided.

  • (V)

    When ‘block.call( E )` is `nil` or `false` for all `E` in `enum` and `ifnone` is a lambda that returns `DEFAULT`.

  • (R)

    The first result ‘RESLUT = block.call( E )` where `RESLUT` is not `nil` or `false`.



41
42
43
44
45
46
47
48
49
50
51
# File 'lib/nrser/functions/enumerable/find_map.rb', line 41

def self.find_map enum, ifnone = nil, &block
  enum.each do |entry|
    if result = block.call( entry )
      # Found a match, short-circuit
      return result
    end
  end
  
  # No matches, return `ifnone`
  ifnone.call if ifnone
end

.find_only(enum, &block) ⇒ E

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 find_bounded with ‘bounds = 1`.

Parameters:

  • enum (Enumerable<E>)

    The entries to search and check.

  • &block (Proc)

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

Returns:

  • (E)

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

Raises:

  • (TypeError)

    If ‘&block` matched more or less than one entry.



153
154
155
# File 'lib/nrser/functions/enumerable.rb', line 153

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) ⇒ nil, ...

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.

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.

Parameters:

  • rel_path (String | Pathname)

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

  • from: (String | Pathname) (defaults to: Pathname.pwd)

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

  • glob: (Boolean | :guess) (defaults to: :guess)

    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*.

  • test: (Symbol) (defaults to: :exist?)

    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.

  • result: (Symbol) (defaults to: :common_root)

    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.

Returns:

  • (nil)

    When no match is found.

  • (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.

  • (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.



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/nrser/functions/path.rb', line 100

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 = find_map( pn_from( from ).ascend ) { |dir|
    path = dir / rel_path
    
    found_path = if glob
      Pathname.glob( path ).find { |match_path|
        match_path.public_send test
      }
    else
      path.public_send test
    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) ⇒ Object

Exactly like find_up but raises if nothing is found.



141
142
143
144
145
146
147
# File 'lib/nrser/functions/path.rb', line 141

def self.find_up! *args
  find_up( *args ).tap { |result|
    if result.nil?
      raise "HERE! #{ args.inspect }"
    end
  }
end

.format_exception(e) ⇒ String

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.

Parameters:

  • e (Exception)

    Exception to format.

Returns:

  • (String)


13
14
15
# File 'lib/nrser/functions/exception.rb', line 13

def self.format_exception e
  "#{ e.message } (#{ e.class }):\n  #{ e.backtrace.join("\n  ") }"
end

.guess_label_key_type(keyed) ⇒ nil, Class

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

Parameters:

  • keyed (#keys & #empty)

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

Returns:

  • (nil)

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

  • (Class)

    If we can determine that String or Symbol keys are exclusively used returns that class.



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 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) ⇒ Boolean

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

Parameters:

  • object (Object)

    Any old thing.

Returns:

  • (Boolean)

    ‘true` if `object` is “hash-like” for our purposes.



32
33
34
35
# File 'lib/nrser/functions/enumerable.rb', line 32

def self.hash_like? object
  object.is_a?( ::Enumerable ) &&
    object.respond_to?( :each_pair )
end

.indent(text, amount = 2, indent_string: nil, indent_empty_lines: false, skip_first_line: false) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 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) ⇒ String

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 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` - INDENT_TAG_MARKER, the no-printable ASCII *record separator* (ASCII character 30, “x1E” / “u001E”).

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

Examples:

With default marker and separator

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

Parameters:

  • text (String)

    String text to indent tag.

  • marker: (String) (defaults to: INDENT_TAG_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.

  • separator: (String) (defaults to: INDENT_TAG_SEPARATOR)

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

Returns:

  • (String)

    Tagged text.



121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/nrser/functions/text/indentation.rb', line 121

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) ⇒ String

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

Parameters:

  • text (String)

    Tagged text string.

  • marker: (String) (defaults to: INDENT_TAG_MARKER)

    Must be the marker used to tag the text.

  • separator: (String) (defaults to: INDENT_TAG_SEPARATOR)

    Must be the separator used to tag the text.

Returns:

  • (String)

    Final text with interpolation and indent correction.



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/nrser/functions/text/indentation.rb', line 151

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) ⇒ Boolean

Returns:

  • (Boolean)


22
23
24
# 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) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/nrser/functions/string.rb', line 65

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) ⇒ Hash<Array, Object>

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

Examples:

Simple “flat” hash


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

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',
# }

Parameters:

  • tree (#each_pair | (#each_index & #each_with_index))

Returns:

  • (Hash<Array, Object>)


51
52
53
54
55
# File 'lib/nrser/functions/tree/leaves.rb', line 51

def self.leaves tree
  {}.tap { |results|
    _internal_leaves tree, path: [], results: results
  }
end

.lines(text) ⇒ Object

Functions



40
41
42
43
44
45
46
47
48
49
# 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.name }"
  end
end

.looks_globish?(path) ⇒ return_type

TODO:

Document glob? method.

Returns @todo Document return value.

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



23
24
25
# File 'lib/nrser/functions/path.rb', line 23

def self.looks_globish? path
  %w|* ? [ {|.any? &path.to_s.method( :include? )
end

.looks_like_json_array?(string) ⇒ Boolean

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 `]`.

Parameters:

  • string (String)

    String to test.

Returns:

  • (Boolean)

    ‘true` if we think `string` encodes a JSON array.



40
41
42
# File 'lib/nrser/functions/string/looks_like.rb', line 40

def self.looks_like_json_array? string
  !!( string =~ JSON_ARRAY_RE )
end

.map(object) { ... } ⇒ Object

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

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

Parameters:

  • object (Object)

    Target object.

Yields:

  • Each element of a collection or the target object itself.

Returns:

  • (Object)

    The result of mapping or applying the block.



89
90
91
92
93
94
95
# 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) {|key, value| ... } ⇒ Array | Hash

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.

Note:

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

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 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.

Parameters:

  • tree (#each_pair | (#each_index & #each_with_index))

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

Yield Parameters:

  • key (Object)

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

  • value (Object)

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

Yield Returns:

  • (Array)

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

Returns:

  • (Array | Hash)

    If no block is provided.

Raises:

  • (TypeError | NoMethodError)

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

  • (ArgumentError)

    If ‘&block` is not provided.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# 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) ⇒ Object



5
6
7
8
9
# 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) {|element| ... } ⇒ Object

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 map_leaves.

Recursively descend through a tree mapping all non-structural elements

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 transformer stuff this was written for).

See the specs for examples. Used in transformer.

Parameters:

  • prune: (Boolean) (defaults to: false)

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

  • tree (#each_pair | (#each_index & #each_with_index))

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

Yield Parameters:

  • element (Object)

    Anything reached from the root that is not structural (hash-like or array-like), including / inside hash keys (though array indexes are not passed).



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# 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

.map_values(enum) {|key, value| ... } ⇒ Hash

Maps an enumerable object to a new hash with the same keys and values obtained by calling ‘block` with the current key and value.

If ‘enumerable` *does not* respond to `#to_pairs` then it’s treated as a hash where the elements iterated by ‘#each` are it’s keys and all it’s values are ‘nil`.

In this way, map_values handles Hash, Array, Set, OpenStruct, and probably pretty much anything else reasonable you may throw at it.

Parameters:

  • enum (#each_pair, #each)

Yield Parameters:

  • key (Object)

    The key that will be used for whatever value the block returns in the new hash.

  • value (nil, Object)

    If ‘enumerable` responds to `#each_pair`, the second parameter it yielded along with `key`. Otherwise `nil`.

Yield Returns:

  • (Object)

    Value for the new hash.

Returns:

  • (Hash)

Raises:

  • (TypeError)

    If ‘enumerable` does not respond to `#each_pair` or `#each`.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/nrser/functions/enumerable.rb', line 66

def self.map_values enum, &block
  result = {}
  
  if enum.respond_to? :each_pair
    enum.each_pair { |key, value|
      result[key] = block.call key, value
    }
  elsif enum.respond_to? :each
    enum.each { |key|
      result[key] = block.call key, nil
    }
  else
    raise ArgumentError.new erb binding, <<-END
      First argument to {NRSER.map_values} must respond to #each_pair or #each
      
      Received
          
          <%= enum.pretty_inspect %>
      
      of class <%= enum.class %>
    END
  end
  
  result
end

.merge_by(current, *updates, &merge_key) ⇒ Array<Hash>

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

Uses deep_merge! to merge.

Parameters:

  • current (Array<Hash>)

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

  • *updates (Array<Hash>)

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

  • &merge_key (Proc<(Hash)=>Object>)

    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`).

Returns:

  • (Array<Hash>)

    Final array of merged hashes. Don’t depend on order.



23
24
25
26
27
# File 'lib/nrser/functions/merge_by.rb', line 23

def self.merge_by current, *updates, &merge_key
  updates.reduce( to_h_by current, &merge_key ) { |result, update|
    deep_merge! result, to_h_by( update, &merge_key )
  }.values
end

.message(*args, &block) ⇒ NRSER::Message

Creates a new Message from the array.

Examples:


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

Returns:



13
14
15
16
17
18
19
# 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

.only(enum, default: nil) ⇒ E, D

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

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

Parameters:

  • enum (Enumerable<E>)

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

  • default: (D) (defaults to: nil)

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

Returns:

  • (E)

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

  • (D)

    When ‘enum` does not have `#count == 1`.



175
176
177
178
179
180
181
# File 'lib/nrser/functions/enumerable.rb', line 175

def self.only enum, default: nil
  if enum.count == 1
    enum.first
  else
    default
  end
end

.only!(enum) ⇒ E

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

Parameters:

  • enum (Enumerable<E>)

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

Returns:

  • (E)

    First element of ‘enum`.

Raises:

  • (ArgumentError)

    If ‘enum` does not have `#count == 1`.



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/nrser/functions/enumerable.rb', line 195

def self.only! enum
  unless enum.count == 1
    raise ArgumentError.new erb binding, <<-END
      Expected enumerable to have #count == 1 but it has
      
      #count = <%= enum.count %>
      
      Enumerable (class: <%= enum.class %>):
      
          <%= enum.pretty_inspect %>
      
    END
  end
  
  enum.first
end

.pn_from(path) ⇒ Pathname

Returns:

  • (Pathname)


6
7
8
9
10
11
12
# File 'lib/nrser/functions/path.rb', line 6

def self.pn_from path
  if path.is_a? Pathname
    path
  else
    Pathname.new path
  end
end

.private_sender(symbol, *args, &block) ⇒ Proc

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.

Examples:


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

Returns:

  • (Proc)


63
64
65
# File 'lib/nrser/functions/proc.rb', line 63

def self.private_sender symbol, *args, &block
  message( symbol, *args, &block ).to_proc publicly: false
end

.public_sender(symbol, *args, &block) ⇒ Proc

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.

Examples:


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

Returns:

  • (Proc)


39
40
41
# File 'lib/nrser/functions/proc.rb', line 39

def self.public_sender symbol, *args, &block
  message( symbol, *args, &block ).to_proc
end

.rest(array) ⇒ return_type

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

Parameters:

  • array (Array)

Returns:

  • (return_type)

    New array consisting of all elements after the first.



11
12
13
# File 'lib/nrser/functions/array.rb', line 11

def self.rest array
  array[1..-1]
end

.retriever(key) ⇒ Proc

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

Parameters:

  • key (String | Symbol | Integer)

    Key (or index) to retrieve.

Returns:

  • (Proc)


108
109
110
# File 'lib/nrser/functions/proc.rb', line 108

def self.retriever key
  ->( indexed ) { indexed[key] }
end

.slice_keys(hash, *keys) ⇒ Object

Lifted from ActiveSupport.



13
14
15
16
17
18
19
20
21
22
# File 'lib/nrser/functions/hash/slice_keys.rb', line 13

def self.slice_keys hash, *keys
  # We're not using this, but, whatever, leave it in...
  if hash.respond_to?(:convert_key, true)
    keys.map! { |key| hash.send :convert_key, key }
  end
  
  keys.each_with_object(hash.class.new) { |k, new_hash|
    new_hash[k] = hash[k] if hash.has_key?(k)
  }
end

.slice_keys!(hash, *keys) ⇒ Object

Meant to be a drop-in replacement for the ActiveSupport version, though I’ve changed the implementation a bit… because honestly I didn’t understand why they were doing it the way they do :/



32
33
34
35
36
37
38
39
40
41
# File 'lib/nrser/functions/hash/slice_keys.rb', line 32

def self.slice_keys! hash, *keys
  # We're not using this, but, whatever, leave it in...
  if hash.respond_to?(:convert_key, true)
    keys.map! { |key| hash.send :convert_key, key }
  end
  
  slice_keys( hash, *keys ).tap { |slice|
    except_keys! hash, *keys
  }
end

.smart_ellipsis(string, max, omission: UNICODE_ELLIPSIS, split: ', ') ⇒ String

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 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.

Parameters:

  • string (String)

    Source string.

  • max (Fixnum)

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

  • omission: (String) (defaults to: UNICODE_ELLIPSIS)

    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.

  • split: (String) (defaults to: ', ')

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

Returns:

  • (String)

    String of at most ‘max` length with the middle chopped out if needed to do so.



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/nrser/functions/string.rb', line 199

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

.squish(str) ⇒ Object

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

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



23
24
25
# File 'lib/nrser/functions/string.rb', line 23

def self.squish str
  str.gsub(/[[:space:]]+/, ' ').strip
end

.stringify_keys(hash) ⇒ Hash<String, *>

Returns a new hash with all keys transformed to strings by calling ‘#to_s` on them.

Lifted from ActiveSupport.

Parameters:

  • hash (Hash)

Returns:

  • (Hash<String, *>)


33
34
35
# File 'lib/nrser/functions/hash/stringify_keys.rb', line 33

def self.stringify_keys hash
  transform_keys hash, &:to_s
end

.stringify_keys!(hash) ⇒ Hash<String, *>

Converts all keys into strings by calling ‘#to_s` on them. **Mutates the hash.**

Lifted from ActiveSupport.

Parameters:

  • hash (Hash)

Returns:

  • (Hash<String, *>)


17
18
19
# File 'lib/nrser/functions/hash/stringify_keys.rb', line 17

def self.stringify_keys! hash
  transform_keys! hash, &:to_s
end

.symbolize_keys(hash) ⇒ Hash

Returns a new hash with all keys that respond to ‘#to_sym` converted to symbols.

Lifted from ActiveSupport.

Parameters:

  • hash (Hash)

Returns:

  • (Hash)

See Also:



36
37
38
39
# File 'lib/nrser/functions/hash/symbolize_keys.rb', line 36

def self.symbolize_keys hash
  # File 'lib/active_support/core_ext/hash/keys.rb', line 54
  transform_keys(hash) { |key| key.to_sym rescue key }
end

.symbolize_keys!(hash) ⇒ Hash

Mutates ‘hash` by converting all keys that respond to `#to_sym` to symbols.

Lifted from ActiveSupport.

Parameters:

  • hash (Hash)

Returns:

  • (Hash)

See Also:



18
19
20
# File 'lib/nrser/functions/hash/symbolize_keys.rb', line 18

def self.symbolize_keys! hash
  transform_keys!(hash) { |key| key.to_sym rescue key }
end

.to_h_by(enum, &block) ⇒ Hash<K, V>

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.

Parameters:

  • enum (Enumerable<V>)

    Enumerable containing the values for the hash.

  • &block (Proc<(V)=>K>)

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

Returns:

  • (Hash<K, V>)

Raises:



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/nrser/functions/enumerable.rb', line 227

def self.to_h_by enum, &block
  enum.each_with_object( {} ) { |element, result|
    key = block.call element
    
    if result.key? key
      raise NRSER::ConflictError.new erb binding, <<-END
        Key <%= key.inspect %> is already in results with value:
        
            <%= result[key].pretty_inspect %>
        
      END
    end
    
    result[key] = element
  }
end

.to_open_struct(hash, freeze: false) ⇒ OpenStruct

Deeply convert a Hash to an OpenStruct.

Parameters:

  • hash (Hash)

Returns:

  • (OpenStruct)

Raises:

  • (TypeError)

    If ‘hash` is not a Hash.



17
18
19
20
21
22
23
24
# File 'lib/nrser/functions/open_struct.rb', line 17

def 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) ⇒ return_type

TODO:

Document transform method.

Returns @todo Document return value.

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



11
12
13
14
15
16
17
18
19
# File 'lib/nrser/functions/tree/transform.rb', line 11

def self.transform tree, source
  map_tree( tree, prune: true ) { |value|
    if value.is_a? Proc
      value.call source
    else
      value
    end
  }
end

.transform_keys(hash, &block) ⇒ Hash

Returns a new hash with each key transformed by the provided block.

Lifted from ActiveSupport.

Parameters:

  • hash (Hash)

Returns:

  • (Hash)

    New hash with transformed keys.

See Also:



35
36
37
38
39
40
41
42
# File 'lib/nrser/functions/hash/transform_keys.rb', line 35

def self.transform_keys hash, &block
  # File 'lib/active_support/core_ext/hash/keys.rb', line 12
  result = {}
  hash.each_key do |key|
    result[yield(key)] = hash[key]
  end
  result
end

.transform_keys!(hash) ⇒ Hash

Lifted from ActiveSupport.

Parameters:

  • hash (Hash)

    Hash to mutate keys.

Returns:

  • (Hash)

    The mutated hash.

See Also:



15
16
17
18
19
20
21
# File 'lib/nrser/functions/hash/transform_keys.rb', line 15

def self.transform_keys! hash
  # File 'lib/active_support/core_ext/hash/keys.rb', line 23
  hash.keys.each do |key|
    hash[yield(key)] = hash.delete(key)
  end
  hash
end

.transformer(&block) ⇒ return_type

TODO:

Document transformer method.

Returns @todo Document return value.

Parameters:

  • arg_name (type)

    @todo Add name param description.

Returns:

  • (return_type)

    @todo Document return value.



51
52
53
54
55
56
57
58
59
# File 'lib/nrser/functions/tree/transform.rb', line 51

def self.transformer &block
  map_tree( block.call SendSerializer.new ) { |value|
    if value.is_a? SendSerializer
      value.to_proc
    else
      value
    end
  }
end

.truncate(str, truncate_at, options = {}) ⇒ Object

Truncates a given text after a given length if text is longer than length:

'Once upon a time in a world far far away'.truncate(27)
# => "Once upon a time in a wo..."

Pass a string or regexp :separator to truncate text at a natural break:

'Once upon a time in a world far far away'.truncate(27, separator: ' ')
# => "Once upon a time in a..."

'Once upon a time in a world far far away'.truncate(27, separator: /\s/)
# => "Once upon a time in a..."

The last characters will be replaced with the :omission string (defaults to “…”) for a total length not exceeding length:

'And they found that many people were sleeping better.'.truncate(25, omission: '... (continued)')
# => "And they f... (continued)"

adapted from

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



108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/nrser/functions/string.rb', line 108

def self.truncate(str, truncate_at, options = {})
  return str.dup unless str.length > truncate_at

  omission = options[:omission] || '...'
  length_with_room_for_omission = truncate_at - omission.length
  stop = \
    if options[:separator]
      str.rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission
    else
      length_with_room_for_omission
    end

  "#{str[0, stop]}#{omission}"
end

.truthy?(object) ⇒ Boolean

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

Parameters:

  • object (nil | String | Boolean)

    Value to test.

Returns:

  • (Boolean)

    ‘true` if the object is “truthy”.

Raises:

  • (ArgumentError)

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

  • (TypeError)

    When ‘object` is not the right type.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# 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) ⇒ V

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 MultipleErrors containing the errors from the block calls.

Parameters:

  • enum (Enumerable<E>)

    Values to call ‘&block` with.

  • &block (Proc<E=>V>)

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

Returns:

  • (V)

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

Raises:

  • (ArgumentError)

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

  • (NRSER::MultipleErrors)

    If all calls to ‘&block` failed.



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/nrser/functions/enumerable.rb', line 323

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

.whitespace?(string) ⇒ Boolean

Returns:

  • (Boolean)


14
15
16
# File 'lib/nrser/functions/string.rb', line 14

def self.whitespace? string
  string =~ WHITESPACE_RE
end

.with_indent_tagged(text, marker: INDENT_TAG_MARKER, separator: INDENT_TAG_SEPARATOR, &interpolate_block) ⇒ String

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

Parameters:

  • marker: (String) (defaults to: INDENT_TAG_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.

  • separator: (String) (defaults to: INDENT_TAG_SEPARATOR)

    Must be the separator used to tag the text.

Returns:

  • (String)

    Final text with interpolation and indent correction.



183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/nrser/functions/text/indentation.rb', line 183

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") ⇒ String

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

Parameters:

  • text (String)

    Text to word wrap.

  • line_width: (Fixnum) (defaults to: 80)

    Line with in number of character to wrap at.

  • break_sequence: (String) (defaults to: "\n")

    String to join lines with.

Returns:

  • (String)

    @todo Document return value.

See Also:



21
22
23
24
25
# 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