Last active
February 21, 2018 00:31
-
-
Save ginjo/2134717faa519a913ec9c974e17bbfa5 to your computer and use it in GitHub Desktop.
Generic tools and libraries for Ginjo projects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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