Saving key string in a hash using a method - ruby

I'm utilizing yaml to create a configuration file to automate creating machine setup files. I'm have some basic ruby scripting experience but looking to start utilizing classes more to make things cleaner and get better at programming.
My YAML names config.yaml:
`machine_configurations:
MACHINE_NAME_1:
Settings:
MACHINE_NAME_2:
Settings:`
I have a class machine_builder.rb
`require 'yaml'
class MachineBuilder
def initialize
#config = YAML.load_file("config.yaml")
end
def machine_list
#config['machine_configurations'].each do |k,v|
k
end
end
end
What I'm trying to figure out how to do is to store an array of the machine configuration strings
I've testing trying to use
test = MachineBuilder.new
machine_list = []
machine_list << test.machine_list
what I'm trying to get for a result is
machine_list = ['MACHINE_NAME_1','MACHINE_NAME_2']
but I keep getting the entire hash key and values stored in the array.
machine_list = ['MACHINE_NAME_1 => Settings: ...',' MACHINE_NAME_2 => Settings...']
I've tried changing the method using the following but I guess I'm missing something.
def machine_list
#config['machine_configurations'].each do |k,v|
return k
end
end
This attempt only returns one value, and I'm assuming that this is because return exits the loop once the one value is found.
def machine_list
#config['machine_configurations'].each do |k,v|
puts k
end
end
I guess in the end I'm also trying to figure out what is the best practice in to iterate and return values in a method or help to better understand using methods and returning values using the methods.

The each method returns the original enumerable object it was called upon, this is why you keep getting the entire hash when you call the machine_list method.
You can try the following code to get an array of the keys of the #config hash:
def machine_list
#config['machine_configurations'].keys
end
and then:
test = MachineBuilder.new
machine_list = test.machine_list
this way the result will be:
machine_list = ['MACHINE_NAME_1','MACHINE_NAME_2']

Related

Exporting methodcalls to file

I'm fairly new to ruby and wanted to know if it's possible to export a method/method call.
Basically, I have this:
class B
def initialize
#bptimer = BP_Timer.new("MyName", method(:sayhello))
end
def sayhello()
msgbox("hello")
end
end
class BP_Timer
def initialize(name = nil, method = nil)
#name = name
#time = 0
#repeats = 0
#start_from = 0
#method = method
#active = false
end
def start()
#active = true
$timers.push(self)
end
end
(I removed all the rather uninteresting parts of the code)
Basically, I now have a "BP_Timer" object with the name and method that will later be pushed into a $timers array which I want to save to a file and later on load from when I restart the program. Is it possible to deconstruct the object in such a way that I can store and restore it?
This is not how we usually store/restore the state. The common approach would be to store a configuration and perform the whole initialization again.
That said, you are to store an array of initialization parameters, like ["MyName", :sayhello] in your case to _YAML,JSON`, or any other text format of your choice:
config.yml
bp_timers:
- "MyName": :sayhello
And then on program start you do:
config = YAML.load_file("config.yml")
config["bp_timers"].each do |k, v|
BP_Timer.new(*[k, method(v)]).start
end
Answering your initial question: yes, this is possible to some extent with Marshal class.

Is there a good way to pass "array addresses" to a method?

How can I refactor the following? I have some values stored in my YAML file as nested arrays, but I want to pull all my transactions into two get and set methods. This works, but is obviously limited and bulky. It feels wrong.
module Persistance
#store = YAML::Store.new('store.yml')
def self.get_transaction(key)
#store.transaction { #store[key] }
end
def self.get_nested_transaction(key, sub)
#store.transaction { #store[key][sub] }
end
end
Bonus credit: I also have an additional method for incrementing values in my YAML file. Is there a further way to refactor this code? Does it make sense to just pass blocks to a single database accessing method?
Hey I remember thinking about this when I was practicing PStore a little while ago. I didn't figure out a working approach then but I managed to get one now. By the way, yaml/store is pretty cool and you can take credit for introducing me to it.
Anyway, on with the code. Basically here's a couple important concepts:
The #store is similar to a hash in that you can use [] and []= but it's not actually a hash, it's a YAML::Store.
Ruby 2.3 has a method Hash#dig which is kind of the missing puzzle piece here. You provide a list of keys and it treats each as successive keys. You can use this for both get and set, as my code shows
If #store were a true hash that would be the end of it but's not, so for this answer I added a YAML::Store#dig method which has the same usage as the original.
require 'yaml/store'
class YAML::Store
def dig(*keys)
first_val = self[keys.shift]
if keys.empty?
first_val
else
keys.reduce(first_val) do |result, key|
first_val[key]
end
end
end
end
class YamlStore
attr_reader :store
def initialize filename
#store = YAML::Store.new filename
end
def get *keys
#store.transaction do
#store.dig *keys
end
end
def set *keys, val
#store.transaction do
final_key = keys.pop
hash_to_set = keys.empty? ? #store : #store.dig(*keys)
hash_to_set.send :[]=, final_key, val
end
end
end
filename = 'store.yml'
db = YamlStore.new filename
db.set :a, {}
puts db.get :a
# => {}
db.set :a, :b, 1
puts db.get :a, :b
# => 1

Creating a Hash with a method

I am having trouble creating a method to establish a new hash. I know that it is definitely easier just to declare the hash, however I need to create a method. Here is what I have so far and it keeps generating an error message.
def create_new(hash_name)
hash_name = Hash.new
end
This should create and empty hash^
def add_item(hash_name, item_name, item_quantity)
hash_name[:item_name.to_sym] = item_quantity
end
I keep getting an error message on the above code^ I am trying to update this hash and add a new key value pair with a method
p create_new("grocery_list")
This creates a new empty hash^ however when I call it with the below code is says the hash is undefined
add_item(grocery_list, "pizza", "1")
p grocery_list
You could also turn it into a class if you fancy.
class MyHash
attr_reader :hash
def initialize
#hash = Hash.new
end
def [](key)
self.hash[key]
end
def []=(key, value)
self.hash[key.to_sym] = value
end
end
grocery_list = MyHash.new
grocery_list['pizza'] = 1
> grocery_list.hash
=> {:pizza=>1}
in your create_new method, you define a hash_name local variable. This variable does not exist anywhere but the body of your method. That's what seems to confuse you.
You could express better your intent with :
def create_new
Hash.new
end
def add_item(hash, key, value)
hash[key.to_sym] = value
end
In order to get to what you are trying to do, you will have to store the result of your method in some kind of variable in order to use it :
grocery_list = create_new # grocery_list is now a local variable
add_item(grocery_list, 'pizza', 1)

Extended hash wants to load itself from YAML

I'm making a class that's intended to be an intelligent Hash that knows how to load its own values if given a YAML filename and then perform various operations on them. Except that first step is stumping me. Given this code:
class Agent < Hash
def initialize
super
end
def load_from_file(filename)
if (File.file?(filename))
self = YAML.load_file(filename)
end
end
end
...the error message is that one "Can't change the value of self"
How would you make a hash that loads itself from a file?
You're very close. Rather than the self assignment, you just want to use Hash#replace:
class Agent < Hash
def initialize
super
end
def load_from_file(filename)
if (File.file?(filename))
replace YAML.load_file(filename)
end
end
end
#replace replaces the keys and values of the calling hash with they keys and values from the passed hash - exactly what you want in this case. However, be sure that you validate that the YAML data is indeed a Hash before calling #replace.

Read and write YAML files without destroying anchors and aliases

This question has been asked before: Read and write YAML files without destroying anchors and aliases?
I was wondering how to solve that problem with many anchors and aliases?
thanks
The problem here is that anchors and aliases in Yaml are a serialization detail, and so aren’t part of the data after it’s been parsed, so the original anchor name isn’t known when writing the data back out to Yaml. In order to keep the anchor names when round tripping you need to store them somewhere when parsing so that they are available later when serializing. In Ruby any object can have instance variables associated with it, so an easy way to achieve this would be to store the anchor name in an instance variable of the objet in question.
Continuing from the example in the earlier question, for hashes we can change our redifined revive_hash method so that if the hash is an anchor then as well as recording the anchor name in the #st variable so later alises can be recognised, we add the it as an instance variable on the hash.
class ToRubyNoMerge < Psych::Visitors::ToRuby
def revive_hash hash, o
if o.anchor
#st[o.anchor] = hash
hash.instance_variable_set "#_yaml_anchor_name", o.anchor
end
o.children.each_slice(2) { |k,v|
key = accept(k)
hash[key] = accept(v)
}
hash
end
end
Note that this only affects yaml mappings that are anchors. If you want to have other types to keep their anchor name you’ll need to look at psych/visitors/to_ruby.rb and make sure the name is added in all cases. Most types can be included by overriding register but there are a couple of others; search for #st.
Now that the hash has the desired anchor name associated with it, you need to make Psych use it instead of the object id when serializing it. This can be done by subclassing YAMLTree. When YAMLTree processes an object, it first checks to see if that object has been seen already, and emits an alias for it if it has. For any new objects, it records that it has seen the object in case it needs to create an alias later. The object_id is used as the key in this, so you need to override those two methods to check for the instance variable, and use that instead if it exists:
class MyYAMLTree < Psych::Visitors::YAMLTree
# check to see if this object has been seen before
def accept target
if anchor_name = target.instance_variable_get('#_yaml_anchor_name')
if #st.key? anchor_name
oid = anchor_name
node = #st[oid]
anchor = oid.to_s
node.anchor = anchor
return #emitter.alias anchor
end
end
# accept is a pretty big method, call super to avoid copying
# it all here. super will handle the cases when it's an object
# that's been seen but doesn't have '#_yaml_anchor_name' set
super
end
# record object for future, using '#_yaml_anchor_name' rather
# than object_id if it exists
def register target, yaml_obj
anchor_name = target.instance_variable_get('#_yaml_anchor_name') || target.object_id
#st[anchor_name] = yaml_obj
yaml_obj
end
end
Now you can use it like this (unlike the previous question, you don’t need to create a custom emitter in this case):
builder = MyYAMLTree.new
builder << data
tree = builder.tree
puts tree.yaml # returns a string
# alternativelty write direct to file:
File.open('a_file.yml', 'r+') do |f|
tree.yaml f
end
here's a slightly modified version for up to newer versions of the psych gem. before it gave me the following error:
NoMethodError - undefined method `[]=' for #<Psych::Visitors::YAMLTree::Registrar:0x007fa0db6ba4d0>
the register method moved into a subclass of YAMLTree, so this works now with respect to everything what matt says in his answer:
class ToRubyNoMerge < Psych::Visitors::ToRuby
def revive_hash hash, o
if o.anchor
#st[o.anchor] = hash
hash.instance_variable_set "#_yaml_anchor_name", o.anchor
end
o.children.each_slice(2) { |k,v|
key = accept(k)
hash[key] = accept(v)
}
hash
end
end
class MyYAMLTree < Psych::Visitors::YAMLTree
class Registrar
# record object for future, using '#_yaml_anchor_name' rather
# than object_id if it exists
def register target, node
anchor_name = target.instance_variable_get('#_yaml_anchor_name') || target.object_id
#obj_to_node[anchor_name] = node
end
end
# check to see if this object has been seen before
def accept target
if anchor_name = target.instance_variable_get('#_yaml_anchor_name')
if #st.key? anchor_name
oid = anchor_name
node = #st[oid]
anchor = oid.to_s
node.anchor = anchor
return #emitter.alias anchor
end
end
# accept is a pretty big method, call super to avoid copying
# it all here. super will handle the cases when it's an object
# that's been seen but doesn't have '#_yaml_anchor_name' set
super
end
end
I had to further modify the code that #markus posted to work with Psych v2.0.17.
Here's what I ended up with. I hope it helps someone else save quite a bit of time. :-)
class ToRubyNoMerge < Psych::Visitors::ToRuby
def revive_hash hash, o
if o.anchor
#st[o.anchor] = hash
hash.instance_variable_set "#_yaml_anchor_name", o.anchor
end
o.children.each_slice(2) do |k,v|
key = accept(k)
hash[key] = accept(v)
end
hash
end
end
class Psych::Visitors::YAMLTree::Registrar
# record object for future, using '#_yaml_anchor_name' rather
# than object_id if it exists
def register target, node
#targets << target
#obj_to_node[_anchor_name(target)] = node
end
def key? target
#obj_to_node.key? _anchor_name(target)
rescue NoMethodError
false
end
def node_for target
#obj_to_node[_anchor_name(target)]
end
private
def _anchor_name(target)
target.instance_variable_get('#_yaml_anchor_name') || target.object_id
end
end
class MyYAMLTree < Psych::Visitors::YAMLTree
# check to see if this object has been seen before
def accept target
if anchor_name = target.instance_variable_get('#_yaml_anchor_name')
if #st.key? target
node = #st.node_for target
node.anchor = anchor_name
return #emitter.alias anchor_name
end
end
# accept is a pretty big method, call super to avoid copying
# it all here. super will handle the cases when it's an object
# that's been seen but doesn't have '#_yaml_anchor_name' set
super
end
def visit_String o
if o == '<<'
style = Psych::Nodes::Scalar::PLAIN
tag = 'tag:yaml.org,2002:str'
plain = true
quote = false
return #emitter.scalar o, nil, tag, plain, quote, style
end
# visit_String is a pretty big method, call super to avoid copying it all
# here. super will handle the cases when it's a string other than '<<'
super
end
end

Resources