[Converting Ruby to Rust] For Each Loops and HashMap help

I am currently trying to convert a ruby script to rust, but I'm facing some errors with the conversion of for each loops, and my use of HashMaps. I would greatly appreciate some help(I am going to post quite a bit of code, but it's mainly just context).
Ruby Code:

file = File.open("aws_instances", "rb")
tag_file = File.open("aws_tags.sh", "wb")
instances = file.read
instances_list = JSON.parse(instances)
ec2 = {}
usedtypes = {}
untagged = Array[]
instances_list["Reservations"].each do |reservation|
  reservation["Instances"].each do |instance|
    next if instance["InstanceLifecycle"] == "spot"
    name = ""
    az = instance["Placement"]["AvailabilityZone"]
    type = instance["InstanceType"]
    eol = ""
    if instance.has_key?("Tags")
      instance["Tags"].each do |tag|
        name = tag["Value"] if tag["Key"] == "Name"
      end
      instance["Tags"].each do |tag|
        eol = tag["Value"] if tag["Key"] == "upl_expected_eol"
      end
      has_tag = 0
      instance["Tags"].each do |tag|
        if tag["Key"] == "stack"
          has_tag = 1
          break
        end
      end
      if has_tag == 0
        untagged.push(name)
      end

      layer_name = nil
      instance["Tags"].each do |tag|
        if tag["Key"].start_with?('opsworks:layer:')
          layer_name=tag["Value"]
          tag_file.write( "#{$awscmd} ec2 create-tags --region us-west-2 --resources #{instance["InstanceId"]}  --tags Key=opsworks:layer,Value='#{layer_name}'\n")
        end
      end
    end
    key = type # + "/" + az
    ec2[key] = [] if ec2.has_key?(key) == false
    if usedtypes.has_key?(key) == false
      usedtypes[key] = "1"
      #puts "used type #{key}"
    end

    if eol == ""
      ec2[key] << "#{name}"
    else
      ec2[key] << "#{name}:*#{eol}*"
    end
  end
end
tag_file.close

The script essentially opens some files, reads them, JSON parses it, and then loops through the file.
My rust code:

// Open aws_instances
    file = File::open("aws_instances").expect("File can't be opened.");
    // Open aws_tags
    let mut tag_file = File::open("aws_tags.sh").expect("File can't be opened.");

    // Read aws_instances
    let mut instances = String::new(); 
    file.read_to_string(&mut instances).expect("File can't be read");

    // Parse aws_instances as JSON
    let instances_list: serde_json::Value = 
        serde_json::from_str(&instances).expect("Json was not well formatted.");
    
    // Create a new HashMap
    let mut ec2 = HashMap::new();
    let mut usedtypes = HashMap::new(); 

    //Create a new Vector
    let mut untagged = Vec::new(); 

    for reservation in instances_list.get("Reservations"){
        for instance in reservation.get("Instances"){
            if instance["InstanceLifecycle"] == "spot"{
                continue; 
            }
            let mut name = ""; 
            let mut az = &instance["Placement"]["AvailabilityZone"]; 
            let mut r#type = &instance["Type"];
            let mut eol = "";
            if let Some(key) = instance["Tags"].as_str(){
                for tag in instance.get("Tags").iter(){
                    if tag["Key"] == name{
                        name = tag["Value"].as_str().unwrap_or("");
                        println!("{:?}", name); 
                    }
                }
                for tag in instance.get("Tags").iter(){
                    if tag["Key"] == "upl_expected_eol"{
                        eol = tag["Value"].as_str().unwrap_or("");
                        println!("{:?}", eol); 
                    }
                }
                let mut has_tag = 0; 
                for tag in instance.get("Tags").iter(){
                    if tag["Key"] == "stack"{
                        has_tag = 1; 
                        break; 
                    }
                }
                if has_tag == 0{
                    untagged.push(name); 
                }

                let mut layer_name; 
                for tag in instance.get("Tags").iter(){
                    if tag["Key"].starts_with("opsworks:layer:"){
                        layer_name = &tag["Value"];
                        let data = ("aws ec2 create-tags --region us-west-2 --resources {}  --tags Key=opsworks:layer,Value='#{layer_name}'\n", instance["InstanceId"]);
                        file::put("tag_file", &data).expect("Unable to write file");
                    }
                }
                let mut key = r#type;
                let mut arr = Vec::new(); 
                if ec2.contains_key(&key) == false{
                    ec2.insert(key, arr);
                }
                if usedtypes.contains_key(&key) == false{
                    usedtypes.insert(key, "1"); 
                }

                if eol == ""{
                    ec2.get_key_value(&key).push("{}", name);
                }else{
                    ec2.get_key_value(&key).push("{}:*{}*", name, eol);
                }
            }

        }
    }
    drop(tag_file);

I get an error of method not found in 'Value':

if tag["Key"].starts_with("opsworks:layer:")

I am unsure why this is failing because tag["Key"] should be a string and it should be checking if it starts with a specific substring.

This next error says it's not implemented for (&str, Value). I believe this might be due to an obsolete file writing crate?

file::put("tag_file", &data).expect("Unable to write file");

These errors both have the problem of the trait 'Hash' is not implemented for 'Value'. I was looking through the HashMap API, I thought contains_key worked like this. Some clarification would be really helpful on how the method works.

if ec2.contains_key(&key) == false
 if usedtypes.contains_key(&key) == false

Finally, the last error, is method cannot be called on HashMap<&Value, Vec<_>> due to unsatisfied trait bounds. Again, I think I misunderstood the HashMap API. Some clarification on how get_key_value would be really helpful.

ec2.get_key_value(&key).push("{}", name);
ec2.get_key_value(&key).push("{}:*{}*", name, eol);

I believe the main problem for the errors are lack of understanding of the for each loops and the HashMap API. I will continue doing research, but any tips would be helpful. Thanks for your time.

    // Create a new HashMap
    let mut ec2 = HashMap::new();
    let mut usedtypes = HashMap::new(); 

    //Create a new Vector
    let mut untagged = Vec::new(); 

Can you manually provide type signatures for these three variables ?

Right now, the compiler infers these types, and derives an error later on. By manually providing these types, we can "bisect" the error, i.e. either:

  1. the objects being put into the collections do not have the type you expect OR
  2. the type of the collection does not support the op you expect

I believe they should all be of type string. I guess the string objects don't have the operation types I expect.

Debugging is the process of finding out which beliefs are false. Why not just label the variables and re run the compiler ?

These look, well, sus. HashMap::get returns Option<&V>, and Option implements Iterator (returning 0 or 1 items). In other words, reservation here likely has the type &Vec<Reservation> or similar, rather than &Reservation.

You probably want:

for reservation in instances_list.get("Reservations").expect("no Reservations entry!") {
        for instance in reservation.get("Instances").expect("no Instances entry!") {

(here I have chosen to use .expect() which panics on missing keys; for something used in production, you may want different error handling)

Writing in an editor with mouseover type annotations could help here. Some choices include VSCode with the Rust Language Server enabled, or IntelliJ Idea with the IntelliJ Rust plugin.

...additionally, you will have a problem where instances_list is not just a HashMap and reservations is not just a Vec, but both of these are serde_json::Value. At this point, the compiler doesn't know whether the data is supposed to be a vec, map, string, etc.

The HashMap is contained in the Object variant. To extract it, you can use the match keyword:

// here, instances_list is Value
let instances_list = match instances_list {
    serde_json::Value::Object(x) => x,
    _ => panic!("expected an object"),
};

Before this statement, instances_list is a serde_json::Value. After this statement, it will be a HashMap. (...well. Technically it is a serde_json::map::Map, which is similar to HashMap but preserves insertion order. So look at those docs, not the HashMap docs. :P)

Value also provides these helper functions which can be a bit cleaner:

// instances_list is Value here
let instances_list = instances_list.as_object().expect("expected an object");
// now it is HashMap

At some point, you will probably find yourself saying:

Gee, all of these matches (or expects, etc) sure are tedious to write!

This is where serde's derives can help. If you write some static types, you can have serde automatically generate all of the code that checks for certain keys to exist and have certain types, and it will give you statically-typed data while also generating nice error messages for malformed input.

#[derive(serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
struct InstancesList {
    reservations: Vec<Reservation>,
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Reservation {
    instances: Vec<Instance>,
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Instance {
    instance_lifecycle: String,
    placement: Placement,
    r#type: String,
    tags: Vec<InstanceTag>,
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
struct InstanceTag {
    key: String,
    value: String,
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Placement {
    availability_zone: String,
}
let instances_list: InstancesList = 
    serde_json::from_str(&instances).expect("Json was not well formatted.");

// note:   'for x in &thing' is a more idiomatic way
// to write 'for x in thing.iter()', for types that support it.
for reservation in &instances_list.reservations {
    for instance in &reservation.instances {
        if instance.instance_lifecycle == "spot" {
            continue;
        }
        // ...
    }
}
5 Likes

Thank you so much, that helped a lot. I still have a few more questions. With these serde_json::Value structs, how can I check if instance contains the key "Tags", referring to the ruby line:

if instance.has_key?("Tags")

In this case instance is a hashmap looking for the key "Tags". I tried adding another value to the instance JSON value struct:

instance: HashMap<String, String> //so then I could possibly write: 
if instance.contains_key("Tags"){

but it says "method not found in &main::Instance".
Additionally, I am getting an error with:

ec2.get_key_value(&key).insert("{}", name);

It said too many arguments, so I tried putting it in a variable but it expects "&std::string::String, found str". Is this a case where the match statement would fix it?

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.