How to: Converting Instance Variables to YAML in Ruby
Nov 2, 2014(Reposting from my old wiki):
On a recent post in ruby-talk, the question was asked how to convert something to YAML
. Extending this generally to ruby objects, I went searching for something that would work besides a brute-force creation of a Hash.
Introduction
The original request was to be able to generate the following YAML from a result:
desired_output.yaml
:
---
- name: device-1
parameters:
app_folder: deploy_project
app_id: "1"
tar_file: deploy_project.tar.gz
profile_id: "3"
version_id: "2"
classes:
- install
If one were to take that and feed it back into Ruby via YAML.load, one gets the following structure:
[{"name"=>"device-1",
"parameters"=>
{"app_folder"=>"deploy_project",
"app_id"=>"1",
"tar_file"=>"deploy_project.tar.gz",
"profile_id"=>"3",
"version_id"=>"2"},
"classes"=>["install"]}]
So an obvious structure is revealed. However, simply brute-forcing this from a result seemed not quite what I would want, so I went searching.
What happens if you YAML-ize an Object directly?
Let’s say we build a couple of classes that can be used with the above information (ignoring that it may have come from a database for the non).
classes.rb
:
# Classes to convert to yaml
class Deploy
attr_accessor :name, :parameters, :classes
def initialize(name, parameters, classes)
self.name = name
self.parameters = parameters
self.classes = classes
end
end
class Params
attr_accessor :app_folder, :app_id, :tar_file, :profile_id, :version_id
def initialize(app_folder, app_id, tar_file, profile_id, version_id)
self.app_folder = app_folder
self.app_id = app_id
self.tar_file = tar_file
self.profile_id = profile_id
self.version_id = version_id
end
end
If we load up an array with the above classes using the data from the original request, we can see this:
$ pry -r yaml -r ./classes.rb
[1] pry(main)> deployments = Array.new
=> []
[2] pry(main)> deployments << Deploy.new("device-1",
[2] pry(main)* Params.new("deploy_project", "1",
[2] pry(main)* "deploy_project.tar.gz", "3", "2"),
[2] pry(main)* ["install"])
=> [#<Deploy:0x007fa0cf7a3ec0
@classes=["install"],
@name="device-1",
@parameters=
#<Params:0x007fa0cf7a3f38
@app_folder="deploy_project",
@app_id="1",
@profile_id="3",
@tar_file="deploy_project.tar.gz",
@version_id="2">>]
[3] pry(main)> deployments.to_yaml
=> "---\n- !ruby/object:Deploy\n name: device-1\n parameters: !ruby/object:Params\n app_folder: deploy_project\n app_id: '1'\n tar_file: deploy_project.tar.gz\n profile_id: '3'\n version_id: '2'\n classes:\n - install\n"
[4] pry(main)> puts deployments.to_yaml
---
- !ruby/object:Deploy
name: device-1
parameters: !ruby/object:Params
app_folder: deploy_project
app_id: '1'
tar_file: deploy_project.tar.gz
profile_id: '3'
version_id: '2'
classes:
- install
=> nil
[5] pry(main)>
While close, simply YAMLizing an Object doesn’t give us what we want – when loaded, it will look for the classes Deploy and Params to create objects from. We don’t want this, exactly, we just want it in the form originally requested. When reread by an application that does not define these classes, we get an error:
$ pry -r yaml
[1] pry(main)> YAML.load_file("direct_to_yaml_output.yaml")
ArgumentError: undefined class/module Deploy
from /opt/rubies/ruby-2.1.2/lib/ruby/2.1.0/psych/class_loader.rb:53:in `path2class'
[2] pry(main)>
How to just get the instance variables into a Hash?
In this question on stackoverflow, one of the respondents points to the instance_variables
method on Object
in ruby. This is pretty simple, and could be more helpful in a non-Rails environment. I decided to write a recursive version that can be mixed into a class:
instance_valude_extension.rb
:
module InstanceValuesExtension
module InstanceMethods
def instance_values
Hash[
instance_variables.map do |name|
key = name.to_s[1..-1]
value = instance_variable_get(name)
if (value.instance_variables.count > 1 && value.respond_to?(:instance_values))
value = value.instance_values
end
[key, value]
end
]
end
end
def self.included(receiver)
receiver.send :include, InstanceMethods
end
end
Using that same data above, we can get;
example.rb
:
$:.unshift(File.expand_path("../", __FILE__))
require 'yaml'
require 'classes.rb'
require 'instance_values_extension'
Deploy.send(:include, InstanceValuesExtension)
Params.send(:include, InstanceValuesExtension)
deployments = [
Deploy.new(
"device-1",
Params.new(
"deploy_project",
"1",
"deploy_project.tar.gz",
"3",
"2"
),
["install"]
)
]
puts deployments.map(&:instance_values).to_yaml
and Voila!
actual_output.yaml
:
---
- name: device-1
parameters:
app_folder: deploy_project
app_id: '1'
tar_file: deploy_project.tar.gz
profile_id: '3'
version_id: '2'
classes:
- install
This won’t do everything in-and-of-itself. For instance, if any of the instance variables consists of an Array or Hash (or even a Struct), it won’t recurse into them. Refinement will be needed.
Still, an interesting exercise!
Note, also, this is no substitute for using ActiveModel::Serializers
in Rails.
(Source in github.)