From 37dc6dea550cf421705609f91ee7254762b510b1 Mon Sep 17 00:00:00 2001 From: Martin Corino Date: Fri, 5 Jan 2024 13:12:35 +0100 Subject: [PATCH 01/11] bump version for next release --- lib/wx/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wx/version.rb b/lib/wx/version.rb index 96052235..d7833251 100644 --- a/lib/wx/version.rb +++ b/lib/wx/version.rb @@ -3,5 +3,5 @@ # This software is released under the MIT license. module Wx - WXRUBY_VERSION = '0.9.3' + WXRUBY_VERSION = '0.9.4' end From 1e0223d36f6cc59706fc1f3c26e16b12c69f8a14 Mon Sep 17 00:00:00 2001 From: Martin Corino Date: Fri, 5 Jan 2024 14:49:05 +0100 Subject: [PATCH 02/11] make group data dynamic --- lib/wx/core/config.rb | 85 +++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/lib/wx/core/config.rb b/lib/wx/core/config.rb index c13fc989..782b2f87 100644 --- a/lib/wx/core/config.rb +++ b/lib/wx/core/config.rb @@ -330,17 +330,17 @@ module Interface def each_entry(&block) if block_given? - @data.select { |_,v| !v.is_a?(::Hash) }.each(&block) + data.select { |_,v| !v.is_a?(::Hash) }.each(&block) else - ::Enumerator.new { |y| @data.each_pair { |k,v| y << [k,v] if !v.is_a?(::Hash) } } + ::Enumerator.new { |y| data.each_pair { |k,v| y << [k,v] if !v.is_a?(::Hash) } } end end def each_group(&block) if block_given? - @data.select { |_,g| g.is_a?(::Hash) }.each { |k,g| block.call(k, Group.new(self, self.path.dup.push(k), g)) } + data.select { |_,g| g.is_a?(::Hash) }.each { |k,g| block.call(k, Group.new(self, self.path.dup.push(k))) } else - ::Enumerator.new { |y| @data.each_pair { |k,g| y << [k,Group.new(self, self.path.dup.push(k), g)] if g.is_a?(::Hash) } } + ::Enumerator.new { |y| data.each_pair { |k,g| y << [k,Group.new(self, self.path.dup.push(k))] if g.is_a?(::Hash) } } end end @@ -365,7 +365,7 @@ def has_entry?(path_str) return false if segments.empty? entry = segments.pop group_data = if segments.empty? - @data + data else unless abs || root? segments = self.path + segments @@ -387,9 +387,9 @@ def has_group?(path_str) def get(key) key = key.to_s raise ArgumentError, 'No paths allowed' if key.index(ConfigBase::SEPARATOR) - elem = @data[key] + elem = data[key] if elem.is_a?(::Hash) - Group.new(self, self.path.dup.push(key), elem) + Group.new(self, self.path.dup.push(key)) else elem end @@ -398,20 +398,21 @@ def get(key) def set(key, val) key = key.to_s raise ArgumentError, 'No paths allowed' if key.index(ConfigBase::SEPARATOR) - exist = @data.has_key?(key) - elem = exist ? @data[key] : nil + hsh = data + exist = hsh.has_key?(key) + elem = exist ? hsh[key] : nil if val.nil? - @data.delete(key) if exist + hsh.delete(key) if exist nil elsif val.is_a?(::Hash) raise ArgumentError, 'Cannot change existing value entry to group.' if exist && !elem.is_a?(::Hash) - elem = @data[key] = {} unless elem - group = Group.new(self, self.path.dup.push(key), elem) + elem = hsh[key] = {} unless elem + group = Group.new(self, self.path.dup.push(key)) val.each_pair { |k, v| group.set(k, v) } group else raise ArgumentError, 'Cannot change existing group to value entry.' if exist && elem.is_a?(::Hash) - @data[key] = sanitize_value(val) + hsh[key] = sanitize_value(val) end end @@ -420,7 +421,7 @@ def delete(path_str) return nil if segments.empty? last = segments.pop group_data = if segments.empty? - @data + data else unless abs || root? segments = self.path + segments @@ -435,8 +436,9 @@ def rename(old_key, new_key) old_key = old_key.to_s new_key = new_key.to_s raise ArgumentError, 'No paths allowed' if old_key.index(ConfigBase::SEPARATOR) || new_key.index(ConfigBase::SEPARATOR) - if @data.has_key?(old_key) && !@data.has_key?(new_key) - @data[new_key] = @data.delete(old_key) + hsh = data + if hsh.has_key?(old_key) && !hsh.has_key?(new_key) + hsh[new_key] = hsh.delete(old_key) true else false @@ -448,7 +450,7 @@ def read(path_str, output=nil) return nil if segments.empty? last = segments.pop group_data = if segments.empty? - @data + data else unless abs || root? segments = self.path + segments @@ -459,7 +461,7 @@ def read(path_str, output=nil) val = group_data[last] if val.is_a?(::Hash) raise TypeError, "Cannot convert group" unless output.nil? - Group.new(self, segments.dup.push(last), val) + Group.new(self, segments.dup.push(last)) else case when ::String == output || ::String === output @@ -483,7 +485,7 @@ def write(path_str, val) return false if segments.empty? last = segments.pop group_data = if segments.empty? - @data + data else unless abs || root? segments = self.path + segments @@ -499,7 +501,7 @@ def write(path_str, val) elsif val.is_a?(::Hash) raise ArgumentError, 'Cannot change existing value entry to group.' if exist && !elem.is_a?(::Hash) elem = group_data[last] = {} unless elem - group = Group.new(self, segments.dup.push(last), elem) + group = Group.new(self, segments.dup.push(last)) val.each_pair { |k, v| group.set(k, v) } group else @@ -514,7 +516,7 @@ def to_s end def to_h - @data + data end def get_path(path_str) @@ -550,12 +552,16 @@ class Group include Interface - def initialize(parent, path, data) + def initialize(parent, path) @parent = parent @path = path.freeze - @data = data end + def data + self.root.__send__(:get_group_at, @path, create_missing_groups: true, is_pruned: true) + end + protected :data + def root? false end @@ -573,7 +579,7 @@ def parent end def get_group_at(segments, create_missing_groups: false) - root.__send__(:get_group_at, segments) + root.__send__(:get_group_at, segments, create_missing_groups: create_missing_groups) end protected :get_group_at @@ -586,6 +592,11 @@ def initialize(hash = nil) replace(hash) if hash end + def data + @data + end + protected :data + def root? true end @@ -614,19 +625,21 @@ def replace(hash) self end - def get_group_at(segments, create_missing_groups: false) - # prune segments (process relative segments) - segments = segments.inject([]) do |lst, seg| - case seg - when '..' - lst.pop # remove previous - # forget .. - when '.' - # forget - else - lst << seg + def get_group_at(segments, create_missing_groups: false, is_pruned: false) + unless is_pruned + # prune segments (process relative segments) + segments = segments.inject([]) do |lst, seg| + case seg + when '..' + lst.pop # remove previous + # forget .. + when '.' + # forget + else + lst << seg + end + lst end - lst end # find group matching segments segments.inject(@data) do |hsh, seg| From 7147704763fffa81c8ec5f7f01804ff8f6fadd65 Mon Sep 17 00:00:00 2001 From: Martin Corino Date: Fri, 5 Jan 2024 15:12:21 +0100 Subject: [PATCH 03/11] fix dynamic group data; remove value sanitizing --- lib/wx/core/config.rb | 66 +++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/lib/wx/core/config.rb b/lib/wx/core/config.rb index 782b2f87..569cf363 100644 --- a/lib/wx/core/config.rb +++ b/lib/wx/core/config.rb @@ -367,9 +367,7 @@ def has_entry?(path_str) group_data = if segments.empty? data else - unless abs || root? - segments = self.path + segments - end + segments = self.path + segments unless abs || root? get_group_at(segments) end !!(group_data && group_data.has_key?(entry) && !group_data[entry].is_a?(::Hash)) @@ -378,9 +376,7 @@ def has_entry?(path_str) def has_group?(path_str) segments, abs = get_path(path_str) return root? if segments.empty? - unless abs || root? - segments = self.path + segments - end + segments = self.path + segments unless abs || root? !!get_group_at(segments) end @@ -412,7 +408,7 @@ def set(key, val) group else raise ArgumentError, 'Cannot change existing group to value entry.' if exist && elem.is_a?(::Hash) - hsh[key] = sanitize_value(val) + hsh[key] = val end end @@ -423,13 +419,10 @@ def delete(path_str) group_data = if segments.empty? data else - unless abs || root? - segments = self.path + segments - end + segments = self.path + segments unless abs || root? get_group_at(segments, create_missing_groups: false) end - raise ArgumentError, "Unable to resolve path #{segments+[last]}" unless group_data - group_data.delete(last) + group_data ? group_data.delete(last) : nil end def rename(old_key, new_key) @@ -450,20 +443,20 @@ def read(path_str, output=nil) return nil if segments.empty? last = segments.pop group_data = if segments.empty? + segments = self.path.dup unless abs || root? data else - unless abs || root? - segments = self.path + segments - end - get_group_at(segments, create_missing_groups: true) + segments = self.path + segments unless abs || root? + get_group_at(segments) end - raise ArgumentError, "Unable to resolve path #{segments+[last]}" unless group_data - val = group_data[last] + val = group_data ? group_data[last] : nil if val.is_a?(::Hash) raise TypeError, "Cannot convert group" unless output.nil? Group.new(self, segments.dup.push(last)) else case + when val.nil? + val when ::String == output || ::String === output val.to_s when ::Integer == output || ::Integer === output @@ -485,11 +478,10 @@ def write(path_str, val) return false if segments.empty? last = segments.pop group_data = if segments.empty? + segments = self.path.dup unless abs || root? data else - unless abs || root? - segments = self.path + segments - end + segments = self.path + segments unless abs || root? get_group_at(segments, create_missing_groups: true) end raise ArgumentError, "Unable to resolve path #{segments+[last]}" unless group_data @@ -506,7 +498,7 @@ def write(path_str, val) group else raise ArgumentError, 'Cannot change existing group to value entry.' if exist && elem.is_a?(::Hash) - group_data[last] = sanitize_value(val) + group_data[last] = val end end alias :[]= :write @@ -528,21 +520,21 @@ def get_path(path_str) end protected :get_path - def sanitize_value(val) - case val - when TrueClass, FalseClass, Numeric, String - val - else - if val.respond_to?(:to_int) - val.to_int - elsif val.respond_to?(:to_f) - val.to_f - else - val.to_s - end - end - end - protected :sanitize_value + # def sanitize_value(val) + # case val + # when TrueClass, FalseClass, Numeric, String + # val + # else + # if val.respond_to?(:to_int) + # val.to_int + # elsif val.respond_to?(:to_f) + # val.to_f + # else + # val.to_s + # end + # end + # end + # protected :sanitize_value end From ab9f9837258353c7f4c187006e0a1ca55e99e336 Mon Sep 17 00:00:00 2001 From: Martin Corino Date: Fri, 5 Jan 2024 15:12:50 +0100 Subject: [PATCH 04/11] clean up --- lib/wx/core/config.rb | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/wx/core/config.rb b/lib/wx/core/config.rb index 569cf363..daa7bf26 100644 --- a/lib/wx/core/config.rb +++ b/lib/wx/core/config.rb @@ -520,22 +520,6 @@ def get_path(path_str) end protected :get_path - # def sanitize_value(val) - # case val - # when TrueClass, FalseClass, Numeric, String - # val - # else - # if val.respond_to?(:to_int) - # val.to_int - # elsif val.respond_to?(:to_f) - # val.to_f - # else - # val.to_s - # end - # end - # end - # protected :sanitize_value - end class Group From 50f992b1849c00ba92089509a843bcc91db28dcc Mon Sep 17 00:00:00 2001 From: Martin Corino Date: Fri, 5 Jan 2024 15:31:42 +0100 Subject: [PATCH 05/11] support coercion by proc --- lib/wx/core/config.rb | 11 +++++------ lib/wx/doc/config.rb | 2 +- tests/test_config.rb | 4 ++++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/wx/core/config.rb b/lib/wx/core/config.rb index daa7bf26..189765ef 100644 --- a/lib/wx/core/config.rb +++ b/lib/wx/core/config.rb @@ -142,8 +142,8 @@ def read(path_str, output=nil) when ::TrueClass == output || ::FalseClass == output || output == true || output == false val.is_a?(Integer) ? val != 0 : !!val else - raise ArgumentError, "Unknown coercion type #{output.is_a?(::Class) ? output : output.class}" if output - val + raise ArgumentError, "Unknown coercion type #{output.is_a?(::Class) ? output : output.class}" unless output.nil? || output.is_a?(::Proc) + output ? output.call(val) : val end end end @@ -454,9 +454,8 @@ def read(path_str, output=nil) raise TypeError, "Cannot convert group" unless output.nil? Group.new(self, segments.dup.push(last)) else + return val unless val && output case - when val.nil? - val when ::String == output || ::String === output val.to_s when ::Integer == output || ::Integer === output @@ -466,8 +465,8 @@ def read(path_str, output=nil) when ::TrueClass == output || ::FalseClass == output || output == true || output == false val.is_a?(::Integer) ? val != 0 : !!val else - raise ArgumentError, "Unknown coercion type #{output.is_a?(::Class) ? output : output.class}" if output - val + raise ArgumentError, "Unknown coercion type #{output.is_a?(::Class) ? output : output.class}" unless output.nil? || output.is_a?(::Proc) + output ? output.call(val) : val end end end diff --git a/lib/wx/doc/config.rb b/lib/wx/doc/config.rb index fa6961b5..817efb3f 100644 --- a/lib/wx/doc/config.rb +++ b/lib/wx/doc/config.rb @@ -113,7 +113,7 @@ def rename(old_key, new_key) end # By default returns un-coerced value. # Raises exception if incompatible coercion is specified. # @param [String] path_str - # @param [Class,nil] output output type to convert to + # @param [Class,Proc,nil] output output type (or converter proc) to convert to (with) # @return [Boolean,String,Integer,Float,Wx::Config::Group,nil] value entry value def read(path_str, output=nil) end diff --git a/tests/test_config.rb b/tests/test_config.rb index 155ed817..004d0996 100644 --- a/tests/test_config.rb +++ b/tests/test_config.rb @@ -192,6 +192,10 @@ def run_auto_accessor_tests(cfg) cfg.Group1.Group1_2 = { 'Float' => 0.3330 } assert_equal_cfg(0.3330, cfg.Group1.Group1_2.get('Float')) + + assert_equal(0.3330, cfg.read('/Group1/Group1_2/Float').to_f) + assert_equal(0.3330, cfg.read('/Group1/Group1_2/Float', Float)) + assert_equal(0.3330, cfg.read('/Group1/Group1_2/Float', ->(v) { v.to_f })) end def test_basic From 1b087a83eb7df0cd875b8b2b55d5389361ead5c6 Mon Sep 17 00:00:00 2001 From: Martin Corino Date: Fri, 5 Jan 2024 15:35:05 +0100 Subject: [PATCH 06/11] clean up --- lib/wx/core/config.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/wx/core/config.rb b/lib/wx/core/config.rb index 189765ef..2a3c444f 100644 --- a/lib/wx/core/config.rb +++ b/lib/wx/core/config.rb @@ -18,7 +18,7 @@ module Interface def method_missing(sym, *args, &block) unless block_given? || args.size>1 setter = false - key = sym.to_s.sub(/=\z/) { |s| setter = true; '' } + key = sym.to_s.sub(/=\z/) { |_| setter = true; '' } if (!setter && args.empty?) || (!has_group?(key) && setter && args.size==1) if setter return set(key, args.shift) @@ -332,13 +332,13 @@ def each_entry(&block) if block_given? data.select { |_,v| !v.is_a?(::Hash) }.each(&block) else - ::Enumerator.new { |y| data.each_pair { |k,v| y << [k,v] if !v.is_a?(::Hash) } } + ::Enumerator.new { |y| data.each_pair { |k,v| y << [k,v] unless v.is_a?(::Hash) } } end end def each_group(&block) if block_given? - data.select { |_,g| g.is_a?(::Hash) }.each { |k,g| block.call(k, Group.new(self, self.path.dup.push(k))) } + data.select { |_,g| g.is_a?(::Hash) }.each { |k,_| block.call(k, Group.new(self, self.path.dup.push(k))) } else ::Enumerator.new { |y| data.each_pair { |k,g| y << [k,Group.new(self, self.path.dup.push(k))] if g.is_a?(::Hash) } } end @@ -402,7 +402,7 @@ def set(key, val) nil elsif val.is_a?(::Hash) raise ArgumentError, 'Cannot change existing value entry to group.' if exist && !elem.is_a?(::Hash) - elem = hsh[key] = {} unless elem + hsh[key] = {} unless elem group = Group.new(self, self.path.dup.push(key)) val.each_pair { |k, v| group.set(k, v) } group @@ -491,7 +491,7 @@ def write(path_str, val) nil elsif val.is_a?(::Hash) raise ArgumentError, 'Cannot change existing value entry to group.' if exist && !elem.is_a?(::Hash) - elem = group_data[last] = {} unless elem + group_data[last] = {} unless elem group = Group.new(self, segments.dup.push(last)) val.each_pair { |k, v| group.set(k, v) } group From 38b295ab04285846349c5632d58b71e47cf60894 Mon Sep 17 00:00:00 2001 From: Martin Corino Date: Fri, 5 Jan 2024 19:01:51 +0100 Subject: [PATCH 07/11] add support for env var expansion --- lib/wx/core/config.rb | 36 +++++++++++++++++++-- lib/wx/doc/config.rb | 20 ++++++++++++ rakelib/lib/director/config_base.rb | 50 +++++++++++++++++++++++------ 3 files changed, 94 insertions(+), 12 deletions(-) diff --git a/lib/wx/core/config.rb b/lib/wx/core/config.rb index 2a3c444f..54a06eab 100644 --- a/lib/wx/core/config.rb +++ b/lib/wx/core/config.rb @@ -408,7 +408,7 @@ def set(key, val) group else raise ArgumentError, 'Cannot change existing group to value entry.' if exist && elem.is_a?(::Hash) - hsh[key] = val + hsh[key] = sanitize(val) end end @@ -497,7 +497,7 @@ def write(path_str, val) group else raise ArgumentError, 'Cannot change existing group to value entry.' if exist && elem.is_a?(::Hash) - group_data[last] = val + group_data[last] = sanitize(val) end end alias :[]= :write @@ -519,6 +519,27 @@ def get_path(path_str) end protected :get_path + def sanitize(val) + if root.expanding_env_vars? && ::String === val + brackets = '{(' + brackets << '%' if Wx::PLATFORM == 'WXMSW' + val.gsub!(/([\\])?\$([#{brackets}])?(\w+)([})%])?/) do |s| + if $1.nil? && + (($2.nil? && $4.nil?) || + ($2 == '{' && $4 == '}') || + ($2 == '(' && $4 == ')') || + (win && $2 == '%' && $4 == '%')) && + ENV[$3] + $1 ? $1+ENV[$3] : ENV[$3] + else + $1 ? s[1,s.size] : s + end + end + end + val + end + protected :sanitize + end class Group @@ -563,6 +584,7 @@ def get_group_at(segments, create_missing_groups: false) include Interface def initialize(hash = nil) + @expand_env_vars = true @data = {} replace(hash) if hash end @@ -600,6 +622,16 @@ def replace(hash) self end + def is_expanding_env_vars + @expand_env_vars + end + alias :expanding_env_vars? :is_expanding_env_vars + + def set_expand_env_vars(flag) + @expand_env_vars = !!flag + end + alias :expand_env_vars :set_expand_env_vars + def get_group_at(segments, create_missing_groups: false, is_pruned: false) unless is_pruned # prune segments (process relative segments) diff --git a/lib/wx/doc/config.rb b/lib/wx/doc/config.rb index 817efb3f..ddb5eaa7 100644 --- a/lib/wx/doc/config.rb +++ b/lib/wx/doc/config.rb @@ -186,6 +186,16 @@ def clear; end # @return [self] def replace(hash) end + # Returns true if we are expanding environment variables in string values, false otherwise. + # @return [Boolean] + def is_expanding_env_vars; end + alias :expanding_env_vars? :is_expanding_env_vars + + # Determine whether we wish to expand environment variables in string values. + # @param [Boolean] flag enables expanding environment variables if true, disables otherwise + def set_expand_env_vars(flag) end + alias :expand_env_vars :set_expand_env_vars + end # Configuration class for wxRuby which stores it's settings in a (possibly nested) Hash. @@ -225,6 +235,16 @@ def clear; end # @return [self] def replace(hash) end + # Returns true if we are expanding environment variables in string values, false otherwise. + # @return [Boolean] + def is_expanding_env_vars; end + alias :expanding_env_vars? :is_expanding_env_vars + + # Determine whether we wish to expand environment variables in string values. + # @param [Boolean] flag enables expanding environment variables if true, disables otherwise + def set_expand_env_vars(flag) end + alias :expand_env_vars :set_expand_env_vars + end end diff --git a/rakelib/lib/director/config_base.rb b/rakelib/lib/director/config_base.rb index 70780b09..df1cca6b 100644 --- a/rakelib/lib/director/config_base.rb +++ b/rakelib/lib/director/config_base.rb @@ -330,7 +330,7 @@ def setup if (argc < 1 || argc > 1) { - rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + rb_raise(rb_eArgError, "wrong # of arguments (%d for 1)", argc); } wxString name = RSTR_TO_WXSTR(argv[0]); wxConfigPathChanger path(cfg, name); @@ -350,7 +350,7 @@ def setup if (argc < 1 || argc > 1) { - rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + rb_raise(rb_eArgError, "wrong # of arguments (%d for 1)", argc); } wxString key = RSTR_TO_WXSTR(argv[0]); VALUE rc = Qfalse; @@ -372,7 +372,7 @@ def setup if (argc < 2 || argc > 2) { - rb_raise(rb_eArgError, "wrong # of arguments(%d for 2)", argc); + rb_raise(rb_eArgError, "wrong # of arguments (%d for 2)", argc); } wxString key = RSTR_TO_WXSTR(argv[0]); wxString newKey = RSTR_TO_WXSTR(argv[1]); @@ -395,7 +395,7 @@ def setup if (argc < 0 || argc > 0) { - rb_raise(rb_eArgError, "wrong # of arguments(%d for 0)", argc); + rb_raise(rb_eArgError, "wrong # of arguments (%d for 0)", argc); } wxString key; long index = 0; @@ -420,7 +420,7 @@ def setup if (argc < 0 || argc > 0) { - rb_raise(rb_eArgError, "wrong # of arguments(%d for 0)", argc); + rb_raise(rb_eArgError, "wrong # of arguments (%d for 0)", argc); } wxString key; long index = 0; @@ -445,7 +445,7 @@ def setup if (argc < 0 || argc > 2) { - rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + rb_raise(rb_eArgError, "wrong # of arguments (%d for 1)", argc); } bool recurse = argc>0 ? (argv[0] != Qfalse && argv[0] != Qnil) : false; size_t n = cfg->GetNumberOfEntries(recurse); @@ -459,7 +459,7 @@ def setup if (argc < 0 || argc > 2) { - rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + rb_raise(rb_eArgError, "wrong # of arguments (%d for 1)", argc); } bool recurse = argc>0 ? (argv[0] != Qfalse && argv[0] != Qnil) : false; size_t n = cfg->GetNumberOfGroups(recurse); @@ -486,7 +486,7 @@ def setup if (argc < 1 || argc > 1) { - rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + rb_raise(rb_eArgError, "wrong # of arguments (%d for 1)", argc); } wxString path = RSTR_TO_WXSTR(argv[0]); return cfg->HasGroup(path) ? Qtrue : Qfalse; @@ -499,7 +499,7 @@ def setup if (argc < 0 || argc > 2) { - rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + rb_raise(rb_eArgError, "wrong # of arguments (%d for 1)", argc); } return WXSTR_TO_RSTR(cfg->GetPath()); } @@ -511,10 +511,36 @@ def setup if (argc < 0 || argc > 2) { - rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + rb_raise(rb_eArgError, "wrong # of arguments (%d for 1)", argc); } return cfg->DeleteAll() ? Qtrue : Qfalse; } + + static VALUE config_wx_is_expanding_env_vars(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc != 0) + { + rb_raise(rb_eArgError, "No arguments expected"); + } + return cfg->IsExpandingEnvVars() ? Qtrue : Qfalse; + } + + static VALUE config_wx_set_expand_env_vars(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 1 || argc > 1) + { + rb_raise(rb_eArgError, "wrong # of arguments (%d for 1)", argc); + } + bool expand = (argv[0] != Qfalse && argv[0] != Qnil); + cfg->SetExpandEnvVars(expand); + return Qnil; + } __HEREDOC spec.add_wrapper_code <<~__HEREDOC SWIGINTERN void @@ -563,6 +589,10 @@ def setup rb_define_method(g_cConfigWx, "rename", VALUEFUNC(config_wx_rename), -1); rb_define_method(g_cConfigWx, "path", VALUEFUNC(config_wx_path), -1); rb_define_method(g_cConfigWx, "clear", VALUEFUNC(config_wx_clear), -1); + rb_define_method(g_cConfigWx, "is_expanding_env_vars", VALUEFUNC(config_wx_is_expanding_env_vars), -1); + rb_define_alias(g_cConfigWx, "expanding_env_vars?", "is_expanding_env_vars"); + rb_define_method(g_cConfigWx, "set_expand_env_vars", VALUEFUNC(config_wx_set_expand_env_vars), -1); + rb_define_alias(g_cConfigWx, "expand_env_vars=", "set_expand_env_vars"); g_cConfig = rb_define_class_under(mWxCore, "Config", g_cConfigBase); rb_define_alloc_func(g_cConfig, config_allocate); From 0d8d7d57ab6072f6539f963a299e6a0e748f1620 Mon Sep 17 00:00:00 2001 From: Martin Corino Date: Fri, 5 Jan 2024 19:02:02 +0100 Subject: [PATCH 08/11] add env var expansion tests --- tests/test_config.rb | 52 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/tests/test_config.rb b/tests/test_config.rb index 004d0996..c69708fb 100644 --- a/tests/test_config.rb +++ b/tests/test_config.rb @@ -132,6 +132,10 @@ def run_config_tests(cfg) cfg['/Group1/Group1_2'] = { 'Float' => 0.3330 } assert_equal_cfg(0.3330, cfg['/Group1/Group1_2'].get('Float')) + assert_equal(0.3330, cfg.read('/Group1/Group1_2/Float').to_f) + assert_equal(0.3330, cfg.read('/Group1/Group1_2/Float', Float)) + assert_equal(0.3330, cfg.read('/Group1/Group1_2/Float', ->(v) { v.to_f })) + cfg.replace(DEMO_CONFIG) # reset end @@ -193,9 +197,48 @@ def run_auto_accessor_tests(cfg) cfg.Group1.Group1_2 = { 'Float' => 0.3330 } assert_equal_cfg(0.3330, cfg.Group1.Group1_2.get('Float')) - assert_equal(0.3330, cfg.read('/Group1/Group1_2/Float').to_f) - assert_equal(0.3330, cfg.read('/Group1/Group1_2/Float', Float)) - assert_equal(0.3330, cfg.read('/Group1/Group1_2/Float', ->(v) { v.to_f })) + cfg.replace(DEMO_CONFIG) # reset + end + + def run_env_var_tests(cfg) + # by default expansion is on + + # add a number of entries for env var in new group 'Environment' + cfg['/Environment/HOME'] = '$HOME' + cfg['Environment'].USER = Wx::PLATFORM == 'WXMSW' ? '%USERNAME%' : '${USER}' + cfg['/Environment/PATH'] = '$(PATH)' + + assert_equal(ENV['HOME'], cfg.Environment['HOME']) + assert_equal(ENV[Wx::PLATFORM == 'WXMSW' ? 'USERNAME' : 'USER'], cfg['/Environment/USER']) + assert_equal(ENV['PATH'], cfg.Environment.PATH) + + # test escaping + cfg['/Environment/Escaped_HOME'] = '\$HOME' + cfg['/Environment/Escaped_HOME2'] = '\\$HOME' + cfg['/Environment/Escaped_HOME3'] = '\\\$HOME' + + assert_equal('$HOME', cfg.Environment['Escaped_HOME']) + assert_equal('$HOME', cfg.Environment['Escaped_HOME2']) + assert_equal('\$HOME', cfg.Environment['Escaped_HOME3']) + + cfg['/Environment/NONSENSE'] = '${NonExistingLongNonsenseVariable}' + + assert_equal('${NonExistingLongNonsenseVariable}', cfg.Environment['NONSENSE']) + + cfg['/Environment/MULTIPLE'] = "$HOME / #{Wx::PLATFORM == 'WXMSW' ? '%USERNAME%' : '${USER}'}" + + assert_equal("#{ENV['HOME']} / #{Wx::PLATFORM == 'WXMSW' ? ENV['USERNAME'] : ENV['USER']}", cfg.Environment['MULTIPLE']) + + # disable env var expansion + cfg.expand_env_vars = false + begin + cfg['/Environment/HOME'] = '$HOME' + + assert_equal('$HOME', cfg.Environment['HOME']) + ensure + # re-enable + cfg.set_expand_env_vars(true) + end end def test_basic @@ -203,6 +246,7 @@ def test_basic run_config_tests(cfg) run_auto_accessor_tests(cfg) + run_env_var_tests(cfg) end def test_global @@ -218,6 +262,7 @@ def test_global run_config_tests(cfg) run_auto_accessor_tests(cfg) + run_env_var_tests(cfg) cfg_old = Wx::ConfigBase.set(nil) @@ -238,6 +283,7 @@ def test_default_wx run_config_tests(cfg) run_auto_accessor_tests(cfg) + run_env_var_tests(cfg) assert_true(cfg.clear) # cleanup end From d7ce2fd30f3a3d8cc7bbc0f4795c2376fe565bc2 Mon Sep 17 00:00:00 2001 From: Martin Corino Date: Fri, 5 Jan 2024 19:02:45 +0100 Subject: [PATCH 09/11] add config support doc --- lib/wx/doc/extra/14_config.md | 102 ++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 lib/wx/doc/extra/14_config.md diff --git a/lib/wx/doc/extra/14_config.md b/lib/wx/doc/extra/14_config.md new file mode 100644 index 00000000..64ec5280 --- /dev/null +++ b/lib/wx/doc/extra/14_config.md @@ -0,0 +1,102 @@ + + +# 14. Configuration support + +## Introduction + +wxRuby3 fully supports the wxWidgets config classes providing a Ruby-fied interface. + +The config classes provide a way to store some application configuration information providing features +that make them very useful for storing all kinds of small to medium volumes of hierarchically-organized, +heterogeneous data. +In wxWidgets these were especially designed for storing application configuration information and intended to be +mostly limited to that. That meant the information to be stored was intended to be: + +* Typed, i.e. strings, booleans or numbers for the moment. You cannot store binary data, for example. +* Small. For instance, it is not recommended to use the Windows registry (which is the default storage medium on + that platform) for amounts of data more than a couple of kilobytes. +* Not performance critical, neither from speed nor from a memory consumption point of view. + +As you will see wxRuby3 extends the support in this area and provides means forego a lot of these restrictions. + +The config classes also are intended to abstract away a lot platform differences. In this area wxRuby3 extends the +support also. + +## Default configuration support + +When the default, global, config instance is used (by using {Wx::ConfigBase.get} with default argument) this will be +a platform specific instance. On Windows platforms a Windows registry based implementation is used and on other +platforms a text format configuration file. + +wxRuby3 provides a single wrapper class for these with {Wx::ConfigWx}. This is an abstract class that cannot be +instantiated in Ruby which provides a common, Ruby-fied interface supported by all config classes in wxRuby3. + +While wxWidgets does a decent job of abstracting platform differences it is in no way perfect in this area. With the +text format configuration files for example the stored values loose all type information since everything is stored +as strings. This also differs from the registry based implementation where some type information is not lost but some +(like boolean types) is. +This is not a problem when accessing information for which the structure and types are exactly known as the config +classes offer type specific readers (as well as writers) which coerce values to their expected types but may offer +nasty surprises when more reflectively accessing data of which the exact typing and structure is not known. + +In Ruby where we more or less expect to have common API-s that can return or accept any type of object needing to be +type specific is awkward. wxRuby3 works around this as much as possible for the {Wx::ConfigWx} wrapper class but also +provides an alternative config class integrated with the wxWidgets framework that does not suffer from these restrictions. + +## Enhanced Ruby configuration support + +Instead of the default, platform specific, config classes it is also possible to use a custom wxRuby3 extension providing +a config class which is implemented in pure Ruby and integrated in the wxWidgets configuration framework. +To use an instance of this class as the global config instance the {Wx::ConfigBase.create} should be called at application +initialization time with it's `:use_hash_config` keyword argument set to `true` (and possibly, to be sure, it's +`forced_create` argument set to `true` also). Alternatively a {Wx::Config} (or derivative) instance could be explicitly +instantiated in code and assigned as global instance with {Wx::ConfigBase.set}. + +This would create an instance of {Wx::Config} and install that as the global config instance (if no other instance was +yet installed or, overruling that condition, if `forced_create` was set to `true`). + +As the keyword argument indicates {Wx::Config} is a Ruby `Hash` based config class implementation. + +Value objects are stored Ruby-style as-is into it's internal hash table (maintaining full typing) and are also retrieved +as-is by default (to maintain compatibility with the {Wx::ConfigWx} wrapper type coercion options are provided). +Grouping is based of nested `Hash` instances. + +Because of the `Hash` based implementation and lack of (the need for) type coercion the {Wx::Config} class does have **any** +restrictions of the type of data stored. The only possible type restrictions to enforce may come from usage contexts: + +* In case of value entries shared with wxWidgets framework code (like for example entries save by the persistence +framework; see [here](15_persistence.md)) value types should be restricted to those supported by the wxWidget platform +specific classes and correspond to what the framework code expects. +* In case of the need to save/restore the configuration data using a mechanism that imposes type restrictions these +should be applied. + +With {Wx::Config} it would be perfectly alright to store arrays or any kind of arbitrary object (only be aware that `Hash` +instances will always be expected to provide configuration structure by default) as long as these do not conflict with +expectations of framework code or storage mechanisms. + +With the standard Ruby YAML and JSON serialization support this also provides improved platform independent configuration +persistence options with full typing maintainability. + +## Differences between default and enhanced configuration support + +The major difference is, as described above, the absence of type restrictions in the enhanced Ruby config class {Wx::Config}. + +Another difference is that {Wx::Config} will not automatically create missing groups or entries on reading. This will only +happen when writing configuration values. + +A last difference is that the default support is by default backed up by persistent storage (windows registry or file) and +the wxRuby enhanced support only provides in-memory storage (`Hash` instance) by default. + +Persisting configuration data from {Wx::Config} will require coding customized storage and retrieval operations (which is +trivial using standard YAML or JSON support). + +## Differences between wxWidgets config interface and wxRuby + +In wxRuby there is no option to provide a default value argument when reading values. The reasoning is that Ruby itself +provides more than enough options to elegantly provide for defaults with statement options like `var ||= default` or +`var = get('something') || default`. + +As a consequence wxRuby also does not support recording defaults on read operations (and also does not provide the +corresponding option setter/getter in the interface). From 52626e3550ef669000d3a6cd017d0138a138a081 Mon Sep 17 00:00:00 2001 From: Martin Corino Date: Sat, 6 Jan 2024 09:41:46 +0100 Subject: [PATCH 10/11] expand env vars on read not write --- lib/wx/core/config.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/wx/core/config.rb b/lib/wx/core/config.rb index 54a06eab..e7308649 100644 --- a/lib/wx/core/config.rb +++ b/lib/wx/core/config.rb @@ -332,7 +332,7 @@ def each_entry(&block) if block_given? data.select { |_,v| !v.is_a?(::Hash) }.each(&block) else - ::Enumerator.new { |y| data.each_pair { |k,v| y << [k,v] unless v.is_a?(::Hash) } } + ::Enumerator.new { |y| data.each_pair { |k,v| y << [k, expand(v)] unless v.is_a?(::Hash) } } end end @@ -387,7 +387,7 @@ def get(key) if elem.is_a?(::Hash) Group.new(self, self.path.dup.push(key)) else - elem + expand(elem) end end @@ -408,7 +408,7 @@ def set(key, val) group else raise ArgumentError, 'Cannot change existing group to value entry.' if exist && elem.is_a?(::Hash) - hsh[key] = sanitize(val) + hsh[key] = val end end @@ -454,6 +454,7 @@ def read(path_str, output=nil) raise TypeError, "Cannot convert group" unless output.nil? Group.new(self, segments.dup.push(last)) else + val = expand(val) return val unless val && output case when ::String == output || ::String === output @@ -497,7 +498,7 @@ def write(path_str, val) group else raise ArgumentError, 'Cannot change existing group to value entry.' if exist && elem.is_a?(::Hash) - group_data[last] = sanitize(val) + group_data[last] = val end end alias :[]= :write @@ -519,7 +520,7 @@ def get_path(path_str) end protected :get_path - def sanitize(val) + def expand(val) if root.expanding_env_vars? && ::String === val brackets = '{(' brackets << '%' if Wx::PLATFORM == 'WXMSW' @@ -538,7 +539,7 @@ def sanitize(val) end val end - protected :sanitize + protected :expand end From 3f481f8a38b69984f6220d4aa9e1a2b10ae30065 Mon Sep 17 00:00:00 2001 From: Martin Corino Date: Sat, 6 Jan 2024 10:19:05 +0100 Subject: [PATCH 11/11] fix expand --- lib/wx/core/config.rb | 23 +++++++++++++++-------- tests/test_config.rb | 2 -- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/wx/core/config.rb b/lib/wx/core/config.rb index e7308649..9117ee9b 100644 --- a/lib/wx/core/config.rb +++ b/lib/wx/core/config.rb @@ -520,24 +520,31 @@ def get_path(path_str) end protected :get_path + EXPAND_RE = if Wx::PLATFORM == 'WXMSW' + /(\\)?([$][{(]?|%)(\w+)([})%])?/ + else + /(\\)?([$][{(]?)(\w+)([})])?/ + end + private_constant :EXPAND_RE + def expand(val) if root.expanding_env_vars? && ::String === val - brackets = '{(' - brackets << '%' if Wx::PLATFORM == 'WXMSW' - val.gsub!(/([\\])?\$([#{brackets}])?(\w+)([})%])?/) do |s| + val.gsub(EXPAND_RE) do |s| if $1.nil? && - (($2.nil? && $4.nil?) || - ($2 == '{' && $4 == '}') || - ($2 == '(' && $4 == ')') || - (win && $2 == '%' && $4 == '%')) && + (($2[0] == '$' && + ($2.size == 1 && $4.nil?) || + ($2[1] == '(' && $4 == ')') || + ($2[1] == '{' && $4 == '}'))|| + ($2[0] == '%' && $4 == '%')) && ENV[$3] $1 ? $1+ENV[$3] : ENV[$3] else $1 ? s[1,s.size] : s end end + else + val end - val end protected :expand diff --git a/tests/test_config.rb b/tests/test_config.rb index c69708fb..0d2308b2 100644 --- a/tests/test_config.rb +++ b/tests/test_config.rb @@ -232,8 +232,6 @@ def run_env_var_tests(cfg) # disable env var expansion cfg.expand_env_vars = false begin - cfg['/Environment/HOME'] = '$HOME' - assert_equal('$HOME', cfg.Environment['HOME']) ensure # re-enable