Skip to content

Instantly share code, notes, and snippets.

@ginjo
Last active February 21, 2018 00:31
Show Gist options
  • Save ginjo/2134717faa519a913ec9c974e17bbfa5 to your computer and use it in GitHub Desktop.
Save ginjo/2134717faa519a913ec9c974e17bbfa5 to your computer and use it in GitHub Desktop.
Generic tools and libraries for Ginjo projects
# This is a hierarchical config module. It stores instance-specific options
# and overlays them with static options from the main config defaults.
#
# Setup: Config.config(...hash of options for top level defaults...)
# Usage:
#
# class MyClass
# include Config
# end
#
# # Create a new instance and pass hash of options to automatically load
# # into the local config. The loading happens behind the scenes, before
# # any custom 'initialize' method is called.
# inst = MyClass.new(*args, ...hash of options for local instance config...)
#
# inst.config
# # => local options compiled on top of defaults.
#
# There should really only be two levels of config for any object: local and top-level.
#
# NOTES ON OPTIONS TO KEEP YOU FROM GOING MAD:
# * Don't merge! initialization options into local config, let the Config class do that automatically.
# * Don't merge runtime options just to pass them on, unless you're passing them to an
# object in a different Config tree.
# * Don't resolve args against options, until you are at the point where you actually need them.
# * Don't filter options, unless you actually need to at that point in the code.
# * Bottom line: Don't mutate config, options, or args unless you need to at that point in the code!
#
require 'forwardable'
module Config
# If we use this, we have to handle whether its loaded or not - a tricky issue.
#using Refinements
### CLASS LEVEL ###
# Using Config or config at the class level is non-destructive.
# It will always go back to the base config at Config#@defaults
# Top-level config goes in Config.@defaults
singleton_class.send :attr_accessor, :defaults, :allowable_options
@defaults = Hash.new
@allowable_options = []
# Return clean array of allowable_options.
def self.allowable_options
@allowable_options.delete_if(){|x| x[/^\s*\#/]}
@allowable_options.flatten!
@allowable_options.compact!
@allowable_options.uniq!
@allowable_options
end
# Filter given hash with allowable_options (white-list).
def self.filtered_options(options)
options.select(){|k,v| allowable_options.include?(k.to_s)}
end
# A one-off non-destructive merging of options with Config.@defaults.
def self.config(**opts)
@defaults.merge(opts)
end
# Allow classes prepended with Config to send '.config' method to the above Config.config.
def self.prepended(other)
other.singleton_class.extend Forwardable
other.singleton_class.def_delegator self, :config
end
### INSTANCE LEVEL ###
# Using Config or config at the instance level will change data
# for the local instance, if you pass it options.
# If you don't pass anything, it will only read data.
extend Forwardable
# Delegate all calls to 'defaults' to the top-level.
def_delegators self, :defaults, :'defaults='
# Allow writing to local config using 'config='
attr_writer :config
# Read & write config to local instance, returning merge with top-level.
# Read with config[] method.
# Write with config(some:hash, goes:here).
# Do not try to write with config[key]=, as it won't write to the @config var.
def config(**opts)
#puts "#{self}#config opts:'#{opts}'"
return @config_cache if @config_cache && opts.empty?
if opts.any?
(@config ||= Hash.new).merge!(Config.filtered_options(opts)) && @config_cache=nil
end
(defaults || Hash.new).merge(@config ||= Hash.new)
end
def initialize(*args, **opts) #(opts={caller:self})
#puts "#{self}#initialize args:'#{args}', opts:'#{opts}'"
@config ||= Hash.new
config(**opts)
if method(__callee__).super_method.arity != 0
super
end
end
end # Config
module Refinements
refine Hash do
# Extract key-value pairs from self, given list of objects.
# If last object given is hash, it will be the collector for the extracted pairs.
# Extracted pairs are deleted from the original hash (self).
# Returns the extracted pairs as a hash or as the supplied collector hash.
# Attempts to ignore case.
def extract(*keys, **recipient)
#other_hash = args.last.is_a?(Hash) ? args.pop : Hash.new
recipient = recipient.empty? ? Hash.new : recipient
recipient.tap do |other|
self.delete_if {|k,v| (keys.include?(k) || keys.include?(k.to_s) || keys.include?(k.to_s.downcase) || keys.include?(k.to_sym)) || keys.include?(k.to_s.downcase.to_sym) ? recipient[k]=v : nil}
end
end
def filter(*keepers)
select {|k,v| keepers.flatten.include?(k.to_s)}
end
def filter!(*keepers)
select! {|k,v| keepers.flatten.include?(k.to_s)}
end
# Used only in rfm Factory. Do not use otherwise.
def rfm_filter(*args)
options = args.rfm_extract_options!
delete = options[:delete]
self.dup.each_key do |k|
self.delete(k) if (delete ? args.include?(k) : !args.include?(k))
end
end
# Used in Connection.
# Convert hash to Rfm::CaseInsensitiveHash
def to_cih
new = Rfm::CaseInsensitiveHash.new
self.each{|k,v| new[k] = v}
new
end
def mutex
@mutex ||= Mutex.new
end
def [](*args)
mutex.synchronize do
super
end
end
def []=(*args)
mutex.synchronize do
super
end
end
end # refine Hash
refine Object.singleton_class do
# Adds methods to put instance variables in metaclass, plus getter/setters.
# Use this to stow private data in singleton_class instance variables.
def meta_attr_accessor(*names)
meta_attr_reader(*names)
meta_attr_writer(*names)
end
def meta_attr_reader(*names)
names.each do |n|
define_method(n.to_s) {singleton_class.instance_variable_get("@#{n}")}
end
end
def meta_attr_writer(*names)
names.each do |n|
define_method(n.to_s + "=") {|val| singleton_class.instance_variable_set("@#{n}", val)}
end
end
end # refine Object.singleton_class
refine Object do
# TODO: Find a better way to do this without patching Object.
# TODO: Remove this from generic refinements file.
#
# Wrap an object in Array, if not already an Array,
# since XmlMini doesn't know which will be returnd for any particular element.
# See Rfm Layout & Record where this is used.
def rfm_force_array
return [] if self.nil?
self.is_a?(Array) ? self : [self]
end
# This is a way to have 'tap' return a different result.
# You can also use 'break <result>' within any tap block,
# without having to rely on this patch.
# See https://stackoverflow.com/questions/7878687/combinatory-method-like-tap-but-able-to-return-a-different-value/7879071#7879071
def as
yield self
end
end # refine Object
refine Array do
# Taken from ActiveSupport extract_options!.
def extract_options!
last.is_a?(::Hash) ? pop : Hash.new
end
end # refine Array
refine Time do
# Returns array of [date,time] in format suitable for FMP.
def to_fm_components(reset_time_if_before_today=false)
d = self.strftime('%m/%d/%Y')
t = if (Date.parse(self.to_s) < Date.today) and reset_time_if_before_today==true
"00:00:00"
else
self.strftime('%T')
end
[d,t]
end
end # refine Time
refine String do
def title_case
self.gsub(/\w+/) do |word|
word.capitalize
end
end
end # refine String
refine Binding do
# Convenience method to get binding local_variables and local_methods.
# Exmp: binding[:some_method_or_local_var, :arg, more:'args']
def [](name_or_code_string_or_sym, *args)
if args.any?
receiver.send name_or_code_string_or_sym, *args
else
eval "#{name_or_code_string_or_sym}"
end
end
end
refine URI do
def to_hash(new_hash=Hash.new)
instance_variables.inject(new_hash){|rslt,ivar| rslt.merge(ivar.to_s.gsub(/[\@\:]/,'').to_sym => instance_variable_get(ivar))}
end
end
end # Refinements
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment