Processing Dictionaries returned by parseYAMLFile?

I’m writing a class that needs to parse simple YAML configuration files into corresponding sclang data structures and was wondering whether there’s a simple means of converting the Dictionaries returned by String.parseYAMLFile (in which all keys are Strings) into IdentityDictionaries in which all keys are Symbols. Dictionaries may be nested arbitrarily – the structure of the tree can’t be known in advance.

I’m quite clear on why String.parseYAMLFile treats everything as a String, and I’m fine with it being the user’s responsibility to ensure that keys used in the YAML files are unique, valid Symbols. I suspect that I might be missing something obvious that already exists in the class library for this purpose (some undocumented variation on “collect” maybe)? Any suggestions would be much appreciated. Thanks!

d = Dictionary["key"->"value"];
d = d.asAssociations.collectAs({ |a| a.key.asSymbol -> a.value }, IdentityDictionary);
d[\key].postln;

… is the most efficient I can think of. There’s nothing TOTALLY automatic, if that’s what you’re looking for.

Thanks for this. Sorry if I was unclear in my initial post – iterating over the associations in the top-level Dictionary won’t convert any nested Dictionaries, unfortunately.

The YAML files I’m parsing may contain Dictionaries of Dictionaries, nested to arbitrary depth. Here’s a concrete example (this is part of a class I’m writing to make it easier to refer to subsets of s.inputBus and s.outputBus via versioned configuration files that specify what’s patched to what in my studio):

---
input:
  Ensemble:
    numChannels: 34
    devices:
      DCSM:
        baseChannel: 0
        numChannels: 4
      Perfourmer:
        baseChannel: 4
        numChannels: 2
      OB6:
        baseChannel: 6
        numChannels: 2
      ES6:
        baseChannel: 13
        numChannels: 8
      loopback:
        baseChannel: 21
        numChannels: 8

output:
  Ensemble:
    numChannels: 34
    devices:
      default:
        baseChannel: 0
        numChannels: 2
      monitors:
        baseChannel: 0
        numChannels: 2
      ES3:
        baseChannel: 13
        numChannels: 8
      loopback:
        baseChannel: 21
        numChannels: 8

The config file format isn’t fixed at this point, so I’m hoping to convert the Dictionary in a way that doesn’t make any assumptions about the structure of the YAML file other than unique keys in any given dict.

You could use a recursive function. Something like this:

(
var yamlString, dict, convertFunc, idDict;

// recursive function
convertFunc = {arg dict;
	var output = IdentityDictionary.new;
	dict.keysValuesDo { |key, value|
		if (value.class == Dictionary, {
			output.put(key.asSymbol, convertFunc.value(value));
		}, {
			output.put(key.asSymbol, value);
		});
	};
	output;
};

// testing
yamlString = "---
input:
  Ensemble:
    numChannels: 34
    devices:
      DCSM:
        baseChannel: 0
        numChannels: 4
      Perfourmer:
        baseChannel: 4
        numChannels: 2
      OB6:
        baseChannel: 6
        numChannels: 2
      ES6:
        baseChannel: 13
        numChannels: 8
      loopback:
        baseChannel: 21
        numChannels: 8

output:
  Ensemble:
    numChannels: 34
    devices:
      default:
        baseChannel: 0
        numChannels: 2
      monitors:
        baseChannel: 0
        numChannels: 2
      ES3:
        baseChannel: 13
        numChannels: 8
      loopback:
        baseChannel: 21
        numChannels: 8
";

dict = yamlString.parseYAML;
dict.postcs;

idDict = convertFunc.value(dict);
idDict.postcs;
)

Yes, that’s exactly it. In the absence of an already existing method, I knew the solution would be recursive, I was just having trouble visualizing the exact function. Thanks so much!

For anyone who finds this who also needs to clean up Dictionaries created by String.parseYAMLFile, here’s what I ended up with:

+ Dictionary {
    interpretYAMLDict {|interpret=true|
        var dict = IdentityDictionary.new;

        this.keysValuesDo({|k, v|
            v.isKindOf(Dictionary).if {
                dict.add(k.asSymbol -> v.interpretYAMLDict(interpret))
            } {
                dict.add(k.asSymbol -> interpret.if { v.interpret } { v })
            }
        });

        ^dict
    }
}

I haven’t tested interpret for any complex cases yet (just Integers, Floats, Strings, and Symbols so far), but will note that the quoting needed to specify Strings and Symbols in a YAML file looks like:

'"foo"' # String
"'bar'" # Symbol
 \baz   # Symbol

Of course String.parseYAMLFile can return other data structures depending on the structure of the underlying YAML – the above is only meant to deal with nested Dictionaries.

1 Like

Just to bring this full circle, here’s a rough draft of an extension method for String which should handle all the types returned by the YAML parser:

+ String {
    interpretYAMLFile {|interpret=true|
        var mapObj = {|xObj|
            var yObj;

            xObj.class.switch(
                Dictionary, { 
                    yObj = IdentityDictionary.new;
                    xObj.keysValuesDo({|k, v| yObj.add(k.asSymbol -> mapObj.value(v)) }) 
                },
                Array, { yObj = xObj.collect({|v| mapObj.value(v) }) },
                String, { yObj = interpret.if { xObj.interpret } { xObj } }
            );

            yObj
        };

        ^mapObj.value(this.parseYAMLFile)
    }
}

Strangely enough, I’ve been looking into doing the same thing today. Here’s some code I found inside the Ambisonic Toolkit source: https://github.com/ambisonictoolkit/atk-sc3/blob/5fd71b10d1bc0e8b7c7771996fbadc0b48be902f/Classes/HoaMatrix.sc#L130

1 Like