Gem Version

SknUtils

SknUtils is a collection of pure-ruby utility classes and modules, with limited dependencies, to augment the development of Ruby applications. Examples of these utilities in action can be found in my related projects SknServices, SknWebApp, and SknBase.

All classes and modules have RSpec test coverage (90+) of their originally intended use-cases.

Available Classes

Available Class.Methods

Available RSpec Helpers

History

2/3/2020 V5.7.0
Added
* RSpec XML_Matchers to spec/support folders
* Update ConcurrentJobs JobCommands to support HTTP Headers

2/24/2019 V5.5.0
Added
* ConcurrentJobs feature set  
- Executes (HTTP/any) jobs in parallel

1/2/2019 V5.4.1
Added
- Wrappable module for evaluation. 
- Ruby comment # frozen_string_literal, everywhere
 
12/16/2018 V5.4.0
Added :duration() utils to SknUtils module: 
    #duration()            #=> returns start_time value 
    #duration(start_time)  #=> returns elapsed-time as string, "%3.3f seconds"

11/09/2018 V5.3.0
Added two utils to SknUtils module: 
    #catch_exceptions(&blk)  #=> catch all exceptions and retry block x times 
    #as_human_size(12345)    #=> 12 KB
- See related RSpecs

10/17/2018 V5.1.3
Enhanced SknUtils::Configurable to include a #registry method/value at its root, like Clas.registry or Class.root
- Right now these are Class methods only, will update them to survive #new later.

10/15/2018 V5.1.1
Enhanced SknSettings to match 95% of the (Rb)Config.gem public API.

10/13/2018 V5.1.0
Added SknRegistry to handle service and handler registrations.
- Command.class => CommandHandler.class/instance container
- Replaces SknContainer

10/12/2018 V5.0.1
Added SknSuccess/SknFailure as value object to carry return codes vs exceptions
Modified Configurable to mimic Rails.env, Rails.root, and Rails.logger

10/2/2018 V5.0.0
Modified SknContainer (IoC) to only use #register and #resolve as it's public API.
- Inspired by: [Andrew Holland](http://cv.droppages.com)

09/30/2018 V4.0.4
Updated EnvStringHandler class to behave like Rails.root in all respects.

02/04/2018 V4.0.2
Added `bin/install` to copy default settings.yml files to the project's config directory

02/04/2018 V4.0.0
Added SknUtils::CoreObjectExtensions, this module contains those popular Rails ActiveSupport extensions like `:present?`.
- However, it is contructed with the Ruby `:refine` and `using SknUtils::CoreObjectExtensions` constraints, so as not to intefer with existing monkey-patches.
- Simply add `using SknUtils::CoreObjectExtensions` to any class or module you wish to use the `:present?`, `:blank?`, etc methods.

01/2018  V3.6.0
Moved Linked List to my Minimum_Heaps gem.  This repo has a tag of 'lists' documententing the removal
Removed classes and utils not directly related to NestedResult

12/2017  V3.5.0
Made Nokogiri's gem install/load Optional for HashToXml class
Added SknContainer as a globally namespaced sub-class of NestedResult.  SknContainer is a NestedResult which is functional wrapper over a Concurrent::Hash.  Hashes can use anyObject or anyValue as their key:value pairs.  This makes them perfect for use as a DI Container.
Added EnvStringHandler class to augment YAML values in an application settings.yml file.  Rails.env.development? becomes SknSettings.env.development? if the following is added to the settings file.
./config/settings.yml: `env: !ruby/string:EnvStringHandler <%= ENV['RACK_ENV'] %>`

09/2017  V3.4.0
Added HashToXml class which converts complex/simple hashes into XML. pre-req's Nokogiri 1.8.0 or higher, unless Rails present, then uses version included with rails.

08/2017  V3.3.0
Added Linked List classes which implement Single, Double, and Circular linked lists patterns.  LinkedLists are implemented
with method results returning node values or the nodes themselves.

07/2017  V3.1.5
Added SknSettings class for use as a replacement to the popular, but obsolete, Config.gem
SknSettings.load_configuration_basename!(config_file_name-only) or 'Rails.env.to_s' value, will load all the yml files in this order:
./config/settings.yml
./config/settings/<config_name>.yml
./config/settings/<config_name>.local.yml
I also restored SknUtils:ResultBean and SknUtils::PageControls to the classes contained in this gem.  They are simple wrappers
inheriting the NestedResult class.  Also added SknHash class as a wrapper without the SknUtils namespace required or exposed

03/2017  V3.0.0
Added SknUtils::NestedResult to replace, or be an alternate, to ResultBean, GenericBean, PageControls, ValueBean, and AttributeHelper.
NestedResult overcome issues with serialization via Marshal and Yaml/Psych.
NestedResult will properly encode all hash based key/value pairs of input and decodes it via #to_h or #to_json
NestedResult encodes everything given no matter how deeply its nested, unlike the prior version where you had control over nesting.

10/2016  V2.0.6  
Added an SknUtils::NullObject and SknUtils::nullable?(value) extracted from [Avdi Grimm's Confident Code](https://gist.github.com/jschoolcraft/979827)
The NullObject class has great all around utility, check out it's specs!        

08/2016  V2.0.3  
Added an exploritory ActionService class and RSpec test, triggered by reading [Kamil Lelonek](https://blog.lelonek.me/what-service-objects-are-not-7abef8aa2f99#.p64vudxq4)
I don't support his approach, but the CreateTask class caught my attention as a Rubyist.        

12/2015  V2.0  
All references to ActiveRecord or Rails has been removed to allow use in non-Rails environments
as a result serialization is done with standard Ruby Hash serialization methods; by first transforming
object back to a hash using its #to_hash method. 

06/2015  V1.5.1 commit #67ef656
Last Version to depend on Rails (ActiveModel) for #to_json and #to_xml serialization

NestedResult class; dynamic key/value container

A class implementing a Ruby PORO (Plain Old Ruby Object) that can be instantiated at runtime with a hash. Creates a nested object with Dot and Hash notational accessors to each key’s value. Additional key/value pairs can be added post-create by simply assigning it; obj.my_new_var = "some value"

Ruby’s Hash object is already extremely flexible, even more so with the addition of dot-notation. As I work more with Ruby outside of Rails, I’m finding more use cases for the capabilities of this gem. Here are a few examples:

  1. Application settings containers, SknSettings. Loads Yaml file based on ENV['RACK_ENV'] value, or specified file-key.
    • Replaces Config and/or RBConfig Gems for yaml based settings
  2. Substitute for Rails.root, via a little ERB/YAML/Marshal statement in settings.yml file, and a helper class
    • settings.yml (YAML)
      • root: <%= Dir.pwd %>
        • enables SknSettings.root
      • env: !ruby/string:SknUtils::EnvStringHandler <%= ENV.fetch('RACK_ENV', 'development') %>
        • enables SknSettings.env.production?

There are many more use cases for Ruby’s Hash that this gem just makes easier to implement.

Public Components

SknUtils::NestedResult           # Primary Key/Value Container with Dot/Hash notiation support.
    SknHash                      # Wrapper in name only, WITHOUT SknUtils namespace, inherits from SknUtils::NestedResult
    SknUtils::ResultBean         # Wrapper in name only, inherits from SknUtils::NestedResult
    SknUtils::PageControls       # Wrapper in name only, inherits from SknUtils::NestedResult
    SknUtils::DottedHash         # Wrapper in name only, inherits from SknUtils::NestedResult

SknSettings                      # Multi-level application Configuration class, Key/Value Container with Dot/Hash notiation support.
                                 - Reads YAML files from ./config/ directory based by environment (RACK_ENV)
                                 - INITIALIZED WHEN GEM LOADS    

SknUtils::Configurable           # Add Class writers/getters to any Class, think class configuration

SknContainer/SknRegistry         # Key/Value container which #registers and #resolves procs, classes, and/or object
                                 - Allows definition in one place, and label references as needed; dependency injection
                                 - INITIALIZED WHEN GEM LOADS    

SknSuccess                       # Three attribute value containers for consistant return codes   -- #value, #message, #success
                                   - Extra #payload method returns value as NestResult if value is_a Hash 
SknFailure                       # Three attribute value containers for return codes   -- #value, #message, #success

SknUtils::ConcurrentJobs         # Async/Sync Job executor pool with HTTP support
    SknUtils::CommandJSONGet     # HTTP Get Command class expecting `json` return, located inside `job_commands`
    SknUtils::CommandJSONPut     # HTTP Put Command class expecting `json` return, located inside `job_commands`
    SknUtils::CommandJSONPost    # HTTP Post Command class expecting `json` return, located inside `job_commands`
    SknUtils::CommandFORMPost    # HTTP Post Command class expecting `form` data return, located inside `job_commands`
    SknUtils::CommandFORMDelete  # HTTP Delete Command class expecting `form` data return, located inside `job_commands`
    SknUtils::HttpProcessor      # Command driven HTTP processing object supporting GET,PUT,POST, and DELETE
    SknUtils::JobWrapper         # Outer wrapper class to capture catastrophic exceptions (syntax normally)

Public Methods: SknUtils::Configurable module

For making an arbitrary class configurable and then specifying those configuration values. Intended to be used at the application level, similar to the use of Rails, or for classes that would benefit from a configuration value when objects are created from it.

  # (1) First establish the method names of class values desired.
  ################
  # Inside Target component
  ################
  class MyApp
    include SknUtils::Configurable.with(:app_id, :title, :cookie_name) # or {root_enable: false})
    # ... default=true for root|env|logger
  end

  # (2) Set initial values for those class values in a Rails Initializer, or before use.
  ################
  # Inside Initializer
  ################
  MyApp.configure do
    app_id "my_app"
    title "My App"
    cookie_name { "#{app_id}_session" }
  end

  # (2) Or set initial values for those class values when defining the class.
  ################
  # During Definition
  ################
  class MyApp
    include SknUtils::Configurable.with(:app_id, :title, :cookie_name, {root_enable: true})
    # ...
    configure do
      app_id "my_app"
      title "My App"
      cookie_name { "#{app_id}_session" }
    end

    self.logger = Logger.new
    self.env    = ENV.fetch('RACK_ENV', 'development')
    self.root   = Dir.pwd
  end

  # (3) Remember they are class values, use as needed.
  ################
  # Usage:
  ################
  MyApp.config.app_id # ==> "my_app"
  MyApp.logger        # ==> <Logger.class>
  MyApp.registry      # ==> <SknRegistry.instance> or user assigned object
  MyApp.env.test?     # ==> true

  ###############
  # Syntax
  ###############
  # Main Class Attrs  -- defaults
  - root     =  application root directory as Pathname
  - env      =  string value from RACK_ENV
  - registry =  SknRegistry instance
  - logger   =  Assigned Logger instance
  #with(*user_attrs, enable_root: true|false) - defaults to enable of Main Class Attrs
  
  # ##
  # User-Defined Attrs
  # ## 
  MyThing.with(:name1, :name2, ...)

Public Methods: SknContainer and/or SknRegistry class

SknContainer is global constant assigned to an instantiated instance of SknRegistry. SknRegistry can be instantiated in the regular way via SknRegistry.new; and is included in the SknConfigurable root options by default under the key registry.

Either returns the labeled value as the original instance/value or if provided with a proc, the result of calling that proc. To register, and label, a class or object for retrieval, use the following API. Also review the RSpecs for additional useage info.

  #register(key, contents = nil, options = {})
    - example: 
        SknContainer.register(:some_klass, MyClass)                   -- class as value
        SknContainer.register(:the_instance, MyClass.new)             -- Object Instance as value 
        SknContainer.register(:unique_instance, -> {MyClass.new}, call: true)  -- New Object Instance for each #resolve 
        SknContainer.register(:unique_instance2, ->(parms) {MyClass.new(parms)}, call: false)  -- New Object with parms for each #resolve 
        SknContainer.register(:some_proc, -> {MyClass.new}, call: false) -- Return uncalled proc for each #resolve 

        SknContainer                                                  -- #register returns self to enable chaining
            .register(:unique_instance, -> {MyClass.new})
            .register(:the_instance, MyClass.new)
            .register(:some_klass, MyClass)    
        
  #resolve(key)
    - example:
        klass  = SknContainer.resolve(:some_klass) 
        instance = SknContainer.resolve(:some_klass).new
        
        obj_instance1 = SknContainer.resolve(:unique_instance) 
        obj_instance2 = SknContainer.resolve(:unique_instance2).call(parms)
        
        same_instance = SknContainer.resolve(:the_instance)
   
            some_proc = SknContainer.resolve(:some_proc).call

  * Testing Support:
      - #substitute(...) allows you to mock an existing entry. #substitute is an alias for #register_mock
        - SknContainer.substitute(:the_instance, MyClass.new)              
      - #restore! clears all mocked entries.  #restore! is an alias for #unregister_mock!
        - SknContainer.restore!

Public Methods: SknUtils::ConcurrentJobs classes

ConcurrentJobs behaves as a concurrent thread pool by using Concurrent::Promise from the concurrent-ruby gem. Enables the definition of Procs, or any callable class, which will be executed in parrallel with the available jobs loaded into ConcurrentJobs. Meant to reduce user-sensitive response times when multiple APIs must be invoked. Also review the RSpecs for additional useage info.

    SknUtils::ConcurrentJobs
       #call(async: true)               - Instantiate ConcurrentJobs with Async Workers, false for Sync Workers
       #register_jobs(cmds, callable)   - Array of Command to be executed by single callable
       #register_job(&block)            - Adds callable block to internal worker queue
       #render_jobs                     - Collect results from all jobs into a Result object
       #elapsed_time_string             - "0.012 seconds" string showing duration of last #render_jobs call
       
       SknUtils::Result                 - Contains individual results from each job executed
          #success?                     - Determines if any job failed
          #messages                     - Retrieves messages from job results, assumed present when job fails
          #values                       - Returns an array of individual results from job executions

Commands and HttpProcessors are included to demonstrate Job creating patterns. ConcurrentJobs is not restricted to Http calls or the command to command handler pattern. Using the #register_job method you can pass callable BLOCK and it will be executed when #render_jobs is invoked. HttpProcessor is what I needed and triggered me to add this feature.

Example here:

begin
  # CommandJSONPost, CommandFORMGet, CommandJSONGet,
  # CommandJSONPut, CommandFORMDelete
  commands = [
      SknUtils::CommandJSONGet.call(full_url: "http://jsonplaceholder.typicode.com/posts"),
      SknUtils::CommandJSONGet.call(full_url: "https://jsonplaceholder.typicode.com/comments"),
      SknUtils::CommandJSONGet.call(full_url: "https://jsonplaceholder.typicode.com/todos/1"),
      SknUtils::CommandJSONGet.call(full_url: "http://jsonplaceholder.typicode.com/users")
  ]

  # Initialize the queue with Async Workers by default
  provider = SknUtils::ConcurrentJobs.call

  # Populate WorkQueue
  provider.register_jobs(commands, SknUtils::HttpProcessor) # mis-spelling these params result in an immediate exception (line 43 below)

  # Execute WorkQueue
  result = provider.render_jobs

  if result.success?
    puts "Success: true"
    puts "Values: #{result.values}"
    puts "Messages: #{result.messages}"
  else
    puts "Success: false - errors: #{result.messages.join(', ')}"
    puts "Values: #{result.values}"
  end

#  result.values
rescue => e
  $stderr.puts e.message, e.backtrace
end

Public Methods: SknSettings class

SknSettings is a global constant containing an initialized Object of SknUtils::Configuration using defaults. To change the ‘development’.yml default please use the following method early or in the case of Rails in ‘application.rb

  #load_config_basename!(config_name) -- Where config_name is the name of yml files stored in the `./config/settings` directory
  #config_path!(path)                 -- Where path format is './<dirs>/', default is: './config/'
                                         and contains a settings.yml file and a 'path/settings/' directory

Paths ./config must exist.
Paths ./config/settings/, and ./config/environments/ are optional
File ./config/settings.yml must exist and contain a valid YAML file structure.
   
File load/deep_merge sequence:
-------------------------------------------------
 <prepend-somefile-or-hash>                       -- see #prepend_source!(...)
 config/settings.yml
 config/settings/#{environment}.yml
 config/environments/#{environment}.yml    
 config/settings.local.yml
 config/settings/#{environment}.local.yml
 config/environments/#{environment}.local.yml
 <append-somefile-or-hash>                        -- see #add_source!(...)


Public API 
 -------------------------------------------------
 #load_config_basename!(environment_name)          -- self, loads std sequence
 #config_path!(config_root)                        -- self, set config_root for above
 #load_and_set_settings(ordered_filelist)          -- self, sets config_root and loads std sequence
   - Alias: #reload_from_files(ordered_filelist)
 #reload!()                                        -- self, clears and reloads last filelist
 #setting_files(config_root, environment_name)     -- returns ordered filelist
 #add_source!(file_path_or_hash)                   -- self, adds yaml_file or hash to end of filelist (:reload! required)
 #prepend_source!(file_path_or_hash)               -- self, adds yaml_file or hash to start of filelist (:reload! required)
 -------------------------------------------------

Usage
 -------------------------------------------------
 Dot notation of yaml file contents
 SknSettings.version   # => version's value
 -------------------------------------------------

Public Methods: SknUtils::NestedResult, SknHash & SknSettings

Each concrete Class supports the following utility methods:

  #to_hash                       -- returns a hash of current key/value pairs, including nested
  #to_json                       -- returns a json string of current key/value pairs, including nested
  #hash_from(:base_key)          -- exports the internal hash starting with this base level key
  #obj.obj2.hash_from(:base)     -- exports the internal hash starting from this nested base level key

  #[]                            -- returns value of attr, when #[<attr_name_symbol>]
  #[]=(attr, value)              -- assigns value to existing attr, or creates a new key/value pair
  #<attr>                        -- returns value of named attribute
  #<attr> = (value)              -- assigns value to existing attr, or creates a new key/value pair
  #<attr>?                       -- detects true/false presence? of attr, and non-blank existance of attr's value; when #address?
  -- Where <attr> is a key value from the initial hash, or a key that was/will be dynamically added

  #keys                          -- returns array of symbol #keys from current nested level
  #==(other)                            -- alias to #===
  #===(other)                           -- returns true/false from camparison of the two objects
  #eql?(other)                          -- returns true/false from camparison of the two objects

NestedResult Basic features include:

 - provides the hash or dot notation methods of accessing values:

     $ obj = SknUtils::NestedResult.new({value1: "some value", value2: {one: 1, two: "two"}})
     $ x = obj.value1
     $ x = obj.value2.one
     $ x = obj["value1"]
     $ x = obj[:value1]

 - enables serialization:
    Internally supports #to_hash and #to_json

    $ person = SknUtils::NestedResult.new({name: "Bob"})
    $ person.to_hash             # => {"name"=>"Bob"}
    $ person.to_json             # => "{\"name\":\"Bob\"}"
    $ dmp = Marshal.dump(person) # => "\x04\bo:\x1ASknUtils::NestedResult\x06:\n@nameI\"\bBob\x06:\x06ET"
    $ person2 = Marshal.load(dmp) # => #<SknUtils::NestedResult:0x007faede906d40 @name="Bob">

 - post create additions:

    'obj = SknUtils::NestedResult.new({value1: "some value", value2: {one: 1, two: "two"}})
    'x = obj.one'                          --causes NoMethodError
    'x = obj.one = 'some other value'      --creates a new instance value with accessors
    'x = obj.one = {key1: 1, two: "two"}'  --creates a new ***bean as the value of obj.one
    'y = obj.one.two'                      --returns "two"
    'y = obj.one[:two]                     --returns "two"
    'y = obj.one['two']                    --returns "two"

 - supports predicates <attr>? method patterns:  target must exist and have a non-empty/valid value

    $ obj = SknUtils::NestedResult.new({name: "Something", active: false, phone: "2609998888"})'
    $ obj.name?'       # => true         -- true or false, like obj.name.present?
    $ obj.active?      # => true         -- your asking if method exist with a valid value, not what the value is!
    $ obj.street?      # => false

NestedResult Usage:

    drb = SknUtils::NestedResult.new(params)                          Basic dot notation:
    ----------------------------------------------------      -----------------------------------------------------------------

    * params = {one: 1,                                         drb.one      = 1
                two: { one: 1, two: "two"},                     drb.two      = <SknUtils::NestedResult>
                                                                drb.two.two  = 'two'

                three: [ {one: 'one', two: 2},                  drb.three.first.one  = 'one'
                         {three: 'three', four: 4}              drb.three[1].four = 4
                       ],                                       drb.three.last.three = 'three'

                four: [
                        [ {one: 'one', two: 2},                 drb.four.first.first.one  = 'one'
                          {three: 'three', four: 4} ],          drb.four.first.last.four = 4
                        [ { 5: 'five', 6: 'six'},               drb.four[1][0][5] = 'five'     # number keys require hash notation :[]
                          {five: '5', six: 6} ]                 drb.four[1].last.six = 6
                      ],
           'five' => [1, 2, 3]                                  drb.five = [1, 2, 3]
                6 => 'number key'                               drb[6] = 'number key'
               }      
       result =  SknUtils::NestedResult.new({
                           success: true,
                           message: "",
                           payload: {package: 'of key/value pairs from operations'}
       })
       ...

       if result.success && result.payload.package?
        # do something with result.payload
       end
       class MyPackage < SknUtils::NestedResult
          def initialize(params={})
            super
          end

          def additional_method
            # do something
          end
       end

Installation

runtime prereqs:

Add this line to your application’s Gemfile:

gem 'skn_utils'

And then execute: $ bundle install

Or install it yourself as: $ gem install skn_utils

Build

  1. $ git clone git@github.com:skoona/skn_utils.git
  2. $ cd skn_utils
  3. $ gem install bundler
  4. $ bundle install
  5. $ bundle exec rspec
  6. $ gem build skn_utils.gemspec
  7. $ gem install skn_utils
    • Done

Console Workout

Start with building gem first.

$ cd skn_utils
$ bin/console

[1] pry(main)> rb = SknUtils::NestedResult.new({sample: [{one: "one", two: "two"},{one: 1, two: 2}] })
[2] pry(main)> pg = SknHash.new({sample: [{three: 3, four: 4},{five: 'five', two: 'two'}] })
[3] pry(main)> pg.sample.first.three
[4] pry(main)> rb.sample.first.one
[5] pry(main)> rb.sample.first[:one]
[6] pry(main)> rb.hash_from(:sample)
[7] pry(main)> rb.sample?
[8] pry(main)> rb.sample[0].one?
...
[10] pry(main)> cfg = SknSettings
[11] pry(main)> cfg.config_path!('./spec/factories/')
[12] pry(main)> cfg.load_config_basename!('test')
[13] pry(main)> cfg.keys
[14] pry(main)> cfg.Packaging.keys

[n] pry(main)> exit
* Done

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

License

The gem is available as open source under the terms of the MIT License.