From 4213ebef815ffcd9e2c44598de49980390b62653 Mon Sep 17 00:00:00 2001 From: kwatch Date: Sat, 19 Nov 2016 08:12:00 +0900 Subject: [PATCH 1/8] feat(psych): allow to generate custom Hash object This is necessary to create custom object as mapping instead of Hash. For example, if you prefer `hashobj.key` instead of `hashobj['key']`: ## allows `h.foo` instead of `h['foo']` class MagicHash < Hash def method_missing(method, *args) return super unless args.empty? return self[method.to_s] end end ## override to generate MagicHash instead of Hash class MagicVisitor < Psych::Visitors::ToRuby def empty_mapping(o) MagicHash.new end end ## example to access `ydoc.foo` instead of `ydoc['foo']` input = <<'END' tables: - name: admin_users columns: - name: id type: int pkey: true END tree = Psych.parse(input) visitor = MagicVisitor.create ydoc = visitor.accept(tree) p ydoc.tables[0].columns[0].name #=> "name" p ydoc.tables[0].columns[0].type #=> "int" --- lib/psych/visitors/to_ruby.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/psych/visitors/to_ruby.rb b/lib/psych/visitors/to_ruby.rb index fd1c8e6c..fdd1c837 100644 --- a/lib/psych/visitors/to_ruby.rb +++ b/lib/psych/visitors/to_ruby.rb @@ -159,7 +159,7 @@ def visit_Psych_Nodes_Mapping o if Psych.load_tags[o.tag] return revive(resolve_class(Psych.load_tags[o.tag]), o) end - return revive_hash(register(o, {}), o) unless o.tag + return revive_hash(register(o, empty_mapping(o)), o) unless o.tag case o.tag when /^!ruby\/struct:?(.*)?$/ @@ -320,6 +320,11 @@ def visit_Psych_Nodes_Alias o end private + + def empty_mapping o + return {} + end + def register node, object @st[node.anchor] = object if node.anchor object From c9061f662a3d589812c7062454aa344a12bd85dc Mon Sep 17 00:00:00 2001 From: kwatch Date: Sat, 19 Nov 2016 08:32:32 +0900 Subject: [PATCH 2/8] feat(psych): allow custom Hash object for unknown tagged mapping --- lib/psych/visitors/to_ruby.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/psych/visitors/to_ruby.rb b/lib/psych/visitors/to_ruby.rb index fdd1c837..5ecb7e70 100644 --- a/lib/psych/visitors/to_ruby.rb +++ b/lib/psych/visitors/to_ruby.rb @@ -303,7 +303,7 @@ def visit_Psych_Nodes_Mapping o end else - revive_hash(register(o, {}), o) + revive_hash(register(o, empty_mapping(o)), o) end end From 95eab417eecca8ae0d18bf598ba7d54a3c660f5c Mon Sep 17 00:00:00 2001 From: kwatch Date: Sat, 19 Nov 2016 09:01:16 +0900 Subject: [PATCH 3/8] feat(psych): add hook point for mapping key and value This is necessary to generate custom object as mapping instead of Hash according to context. For example: TableObj = Struct.new('TableObj', 'name', 'columns') ColumnObj = Struct.new('ColumnObj', 'name', 'type', 'pkey') class CustomVisitor < Psych::Visitors::ToRuby def initialize(*args) super @key_path = [] # ex: [] -> ['tables'] -> ['tables', 'columns'] end def accept_key(k) # push keys key = super k @key_path << key return key end def accept_value(v) # pop keys value = super v @key_path.pop() return value end def empty_mapping(o) # generate custom object instead of Hash case @key_path.last when 'tables' ; return TableObj.new when 'columns' ; return ColumnObj.new else ; return super o end end end ## example to generate custom object according to context input = <<'END' tables: - name: admin_users columns: - name: id type: int pkey: true END tree = Psych.parse(input) visitor = CustomVisitor.create ydoc = visitor.accept(tree) p ydoc['tables'][0].class #=> Struct::TableObj p ydoc['tables'][0]['columns'][0].class #=> Struct::ColumnObj --- lib/psych/visitors/to_ruby.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/psych/visitors/to_ruby.rb b/lib/psych/visitors/to_ruby.rb index 5ecb7e70..9eb11bf1 100644 --- a/lib/psych/visitors/to_ruby.rb +++ b/lib/psych/visitors/to_ruby.rb @@ -336,11 +336,19 @@ def register_empty object list end + def accept_key k + accept(k) + end + + def accept_value v + accept(v) + end + SHOVEL = '<<' def revive_hash hash, o o.children.each_slice(2) { |k,v| - key = accept(k) - val = accept(v) + key = accept_key(k) + val = accept_value(v) if key == SHOVEL && k.tag != "tag:yaml.org,2002:str" case v From 080fd60ff85859d06ee56b82f06adc8f51870730 Mon Sep 17 00:00:00 2001 From: kwatch Date: Sat, 19 Nov 2016 09:11:49 +0900 Subject: [PATCH 4/8] feat(psych): add hook points for mapping-like object --- lib/psych/visitors/to_ruby.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/psych/visitors/to_ruby.rb b/lib/psych/visitors/to_ruby.rb index 9eb11bf1..cebafeed 100644 --- a/lib/psych/visitors/to_ruby.rb +++ b/lib/psych/visitors/to_ruby.rb @@ -142,7 +142,7 @@ def visit_Psych_Nodes_Sequence o when '!omap', 'tag:yaml.org,2002:omap' map = register(o, Psych::Omap.new) o.children.each { |a| - map[accept(a.children.first)] = accept a.children.last + map[accept_key(a.children.first)] = accept_value a.children.last } map when /^!(?:seq|ruby\/array):(.*)$/ @@ -171,8 +171,8 @@ def visit_Psych_Nodes_Mapping o members = {} struct_members = s.members.map { |x| class_loader.symbolize x } o.children.each_slice(2) do |k,v| - member = accept(k) - value = accept(v) + member = accept_key(k) + value = accept_value(v) if struct_members.include?(class_loader.symbolize(member)) s.send("#{member}=", value) else @@ -215,8 +215,8 @@ def visit_Psych_Nodes_Mapping o string = nil o.children.each_slice(2) do |k,v| - key = accept k - value = accept v + key = accept_key k + value = accept_value v if key == 'str' if klass @@ -258,7 +258,7 @@ def visit_Psych_Nodes_Mapping o set = class_loader.psych_set.new @st[o.anchor] = set if o.anchor o.children.each_slice(2) do |k,v| - set[accept(k)] = accept(v) + set[accept_key(k)] = accept_value(v) end set @@ -271,7 +271,7 @@ def visit_Psych_Nodes_Mapping o revive_hash hash, value when 'ivars' value.children.each_slice(2) do |k,v| - hash.instance_variable_set accept(k), accept(v) + hash.instance_variable_set accept_key(k), accept_value(v) end end end @@ -283,7 +283,7 @@ def visit_Psych_Nodes_Mapping o when '!omap', 'tag:yaml.org,2002:omap' map = register(o, class_loader.psych_omap.new) o.children.each_slice(2) do |l,r| - map[accept(l)] = accept r + map[accept_key(l)] = accept_value r end map From b14d98f75f485972646a33f75b5ba920012664a7 Mon Sep 17 00:00:00 2001 From: kwatch Date: Sat, 19 Nov 2016 09:28:32 +0900 Subject: [PATCH 5/8] feat(psych): add hook points for merge ('<<') --- lib/psych/visitors/to_ruby.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/psych/visitors/to_ruby.rb b/lib/psych/visitors/to_ruby.rb index cebafeed..856bc7d2 100644 --- a/lib/psych/visitors/to_ruby.rb +++ b/lib/psych/visitors/to_ruby.rb @@ -354,7 +354,7 @@ def revive_hash hash, o case v when Nodes::Alias, Nodes::Mapping begin - hash.merge! val + merge_mapping(hash, val) rescue TypeError hash[key] = val end @@ -362,9 +362,9 @@ def revive_hash hash, o begin h = {} val.reverse_each do |value| - h.merge! value + merge_mapping(h, value) end - hash.merge! h + merge_mapping(hash, h) rescue TypeError hash[key] = val end @@ -379,6 +379,10 @@ def revive_hash hash, o hash end + def merge_mapping hash, val + hash.merge! val + end + def merge_key hash, key, val end From b5d38859f074c36822b64ebf4dd207f0eb78652e Mon Sep 17 00:00:00 2001 From: kwatch Date: Sat, 19 Nov 2016 10:19:34 +0900 Subject: [PATCH 6/8] feat(psych): define 'Psych::Visitors::CustomClassVisitor' class This new class allows user to generate custom object instead of Hash. See document of CustomClassVisitor class for details. --- lib/psych/visitors/custom_class.rb | 87 ++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 lib/psych/visitors/custom_class.rb diff --git a/lib/psych/visitors/custom_class.rb b/lib/psych/visitors/custom_class.rb new file mode 100644 index 00000000..5f74b4d2 --- /dev/null +++ b/lib/psych/visitors/custom_class.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: false + +require 'psych/visitors/to_ruby' + + +module Psych + module Visitors + + ## + ## Visitor class to generate custom object instead of Hash. + ## + ## Example1: + ## + ## ## define custom classes + ## Team = Struct.new('Team', 'name', 'members') + ## Member = Struct.new('Member', 'name', 'gender') + ## ## create visitor object + ## require 'psych' + ## require 'psych/visitors/custom_class' + ## classmap = { + ## "teams" => Team, + ## "members" => Member, + ## } + ## visitor = Psych::Visitors::CustomClassVisitor.create(classmap) + ## ## sample YAML document + ## input = <<-'END' + ## teams: + ## - name: SOS Brigade + ## members: + ## - {name: Haruhi, gender: F} + ## - {name: Kyon, gender: M} + ## - {name: Mikuru, gender: F} + ## - {name: Itsuki, gender: M} + ## - {name: Yuki, gender: F} + ## END + ## ## parse YAML document with custom classes + ## tree = Psych.parse(input) + ## ydoc = visitor.accept(tree) + ## p ydoc['teams'][0].class #=> Struct::Team + ## p ydoc['teams'][0]['members'][0].class #=> Struct::Member + ## team = ydoc['teams'][0] + ## p team.name #=> "SOS Brigade" + ## p team.members[0].name #=> "Haruhi" + ## p team.members[0].gender #=> "F" + ## + class CustomClassVisitor < ToRuby + + def self.create(classmap={}) + visitor = super() + visitor.instance_variable_set('@classmap', classmap) + visitor + end + + attr_reader :classmap # key: string, value: class object + + def initialize(*args) + super + @key_path = [] # ex: [] -> ['tables'] -> ['tables', 'columns'] + end + + private + + def accept_key(k) # push keys + key = super k + @key_path << key + key + end + + def accept_value(v) # pop keys + value = super v + @key_path.pop() + value + end + + def empty_mapping(o) # generate custom object (or Hash object) + klass = @classmap[@key_path.last] + klass ? klass.new : super + end + + def merge_mapping(hash, val) # for '<<' (merge) + val.each {|k, v| hash[k] = v } + end + + end + + end +end From b81d44e0c8215407ab35b7af7bed60eb26ba6378 Mon Sep 17 00:00:00 2001 From: kwatch Date: Sat, 19 Nov 2016 10:41:43 +0900 Subject: [PATCH 7/8] feat(psych): support key '*' for default custom class --- lib/psych/visitors/custom_class.rb | 37 +++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/psych/visitors/custom_class.rb b/lib/psych/visitors/custom_class.rb index 5f74b4d2..5feacc73 100644 --- a/lib/psych/visitors/custom_class.rb +++ b/lib/psych/visitors/custom_class.rb @@ -43,6 +43,41 @@ module Visitors ## p team.members[0].name #=> "Haruhi" ## p team.members[0].gender #=> "F" ## + ## Example2: + ## + ## ## allows `hash.foo` instead of `hash["foo"]` + ## class MagicHash < Hash + ## def method_missing(method, *args) + ## return super unless args.empty? + ## return self[method.to_s] + ## end + ## end + ## ## create visitor with custom hash class + ## require 'psych' + ## require 'psych/visitors/custom_class' + ## classmap = {'*' => MagicHash} + ## visitor = Psych::Visitors::CustomClassVisitor.create(classmap) + ## ## sample YAML document + ## input = <<-'END' + ## teams: + ## - name: SOS Brigade + ## members: + ## - {name: Haruhi, gender: F} + ## - {name: Kyon, gender: M} + ## - {name: Mikuru, gender: F} + ## - {name: Itsuki, gender: M} + ## - {name: Yuki, gender: F} + ## END + ## ## parse YAML document with custom hash class + ## tree = Psych.parse(input) + ## ydoc = visitor.accept(tree) + ## p ydoc.class #=> MagicHash + ## p ydoc['teams'][0].class #=> MagicHash + ## p ydoc['teams'][0]['members'][0].class #=> MagicHash + ## p ydoc.teams[0].members[0].name #=> "Haruhi" + ## p ydoc.teams[0].members[0].gender #=> "F" + ## + class CustomClassVisitor < ToRuby def self.create(classmap={}) @@ -73,7 +108,7 @@ def accept_value(v) # pop keys end def empty_mapping(o) # generate custom object (or Hash object) - klass = @classmap[@key_path.last] + klass = @classmap[@key_path.last] || @classmap['*'] klass ? klass.new : super end From 34883b8b8a8819a0597046dd35ad85c8190cb816 Mon Sep 17 00:00:00 2001 From: kwatch Date: Sat, 19 Nov 2016 12:14:58 +0900 Subject: [PATCH 8/8] feat(psych): add test script --- test/psych/visitors/test_custom_class.rb | 98 ++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 test/psych/visitors/test_custom_class.rb diff --git a/test/psych/visitors/test_custom_class.rb b/test/psych/visitors/test_custom_class.rb new file mode 100644 index 00000000..5ac8aeca --- /dev/null +++ b/test/psych/visitors/test_custom_class.rb @@ -0,0 +1,98 @@ +# coding: US-ASCII +# frozen_string_literal: false +require 'psych/helper' +require 'psych/visitors/custom_class' + +module Psych + module Visitors + class TestCustomClass < TestCase + + INPUT_STRING = <<-'END' + teams: + - name: SOS Brigade + members: + - {name: Haruhi, gender: F} + - {name: Kyon, gender: M} + - {name: Mikuru, gender: F} + - {name: Itsuki, gender: M} + - {name: Yuki, gender: F} + END + + def test_custom_classes + classmap = { + "teams" => Struct.new('Team', 'name', 'members'), + "members" => Struct.new('Member', 'name', 'gender'), + } + # + visitor = Psych::Visitors::CustomClassVisitor.create(classmap) + tree = Psych.parse(INPUT_STRING) + ydoc = visitor.accept(tree) + # + assert_kind_of Hash, ydoc + assert_kind_of classmap["teams"], ydoc['teams'][0] + assert_kind_of classmap["members"], ydoc['teams'][0]['members'][0] + # + team = ydoc['teams'][0] + assert_equal 'SOS Brigade', team.name + assert_equal 'Haruhi', team.members[0].name + assert_equal 'F', team.members[0].gender + end + + def test_default_class + magic_hash_cls = Class.new(Hash) do + def method_missing(method, *args) + return super unless args.empty? + return self[method.to_s] + end + end + classmap = {'*' => magic_hash_cls} + # + visitor = Psych::Visitors::CustomClassVisitor.create(classmap) + tree = Psych.parse(INPUT_STRING) + ydoc = visitor.accept(tree) + # + assert_kind_of magic_hash_cls, ydoc + assert_kind_of magic_hash_cls, ydoc['teams'][0] + assert_kind_of magic_hash_cls, ydoc['teams'][0]['members'][0] + # + team = ydoc['teams'][0] + assert_equal "SOS Brigade", team.name + assert_equal "Haruhi", team.members[0].name + assert_equal "F", team.members[0].gender + end + + def test_merge_mapping + input = <<-END + column-defaults: + - &id + name : id + type : int + pkey : true + tables: + - name : admin_users + columns: + - <<: *id + name: user_id + END + # + classmap = { + "tables" => Struct.new('Table', 'name', 'columns'), + "columns" => Struct.new('Column', 'name', 'type', 'pkey', 'required'), + } + # + visitor = Psych::Visitors::CustomClassVisitor.create(classmap) + tree = Psych.parse(input) + ydoc = visitor.accept(tree) + # + assert_kind_of classmap["tables"], ydoc['tables'][0] + assert_kind_of classmap["columns"], ydoc['tables'][0]['columns'][0] + # + table = ydoc['tables'][0] + assert_equal "int", table.columns[0].type # merged + assert_equal true, table.columns[0].pkey # merged + assert_equal "user_id", table.columns[0].name # ovrerwritten + end + + end + end +end