Chris
Chris

Reputation: 85

Returning a struct created by serde_json in a function

I'm stuck on what seems like a simple issue. I get why I am seeing the error but can't seem to resolve it. Obviously I am missing something fundamental.

fn terraform_deploy_info<'a>(app: &'a MyApp) -> std::result::Result<&MyAppDeployInfo, Error> {
    let terraform = process::Command::new("terraform")
          // We are querying output values.
          .arg("output")
          // We want it in json format for easy processing.
          .arg("-json")
          .output()
          .expect("failed to execute terraform");

    let output = String::from_utf8_lossy(&terraform.stdout);
    let data: TerraformOutputs = serde_json::from_str(&output).unwrap();

    let m = data.deploy_info.value.iter().filter(|&x| x.app == "myapp").collect::<Vec<_>>();

    if m.len() > 1 {
        return Err(Error::MultipleDeployInfo);
    }

    match m.get(0) {
        Some(&x) => Ok(x),
        None => Err(Error::NoDeployInfo),
    }
}

The error I get is:

borrowed value must be valid for the lifetime 'a as defined on the body at

Which makes sense to me, as I am creating the struct in the function and returning a borrowed reference, which of course goes away when the function is finished.

But, when I change the return type be std::result::Result<MyAppDeployInfo, Error> (that is, not returning a reference) I can't seem to get Ok(x) to work...I get an error:

expected struct `MyAppDeployInfo`, found reference

Again, this makes sense as serde_json creates a structure and then I iterate through references, so when I index into the collection I am looking at a reference.

So I tried all sorts of things to get the struct value like dereferencing, Box::new, clone(), to_owned(), etc and still can't get it to work.

I've searched all the issues here, read the book, etc and it is still not clear to me how I can resolve this...any pointers would be appreciated.

Upvotes: 4

Views: 763

Answers (2)

oli_obk
oli_obk

Reputation: 31243

Without knowing more about your project (please produce an MCVE next time), I'd say that you can change the .iter() call into .into_iter(). Instead of collecting into a Vec and then using get, I'd simply work with the iterator directly:

let m = data.deploy_info.value.into_iter().filter(|&x| x.app == "myapp").fuse();

match (m.next(), m.next()) {
    (None, None) => Err(Error::NoDeployInfo),
    (Some(x), None) => Ok(x),
    (Some(_), Some(_)) => Err(Error::MultipleDeployInfo),
    (None, Some(_)) => panic!("Iterator::fuse broken"),
}

Upvotes: 3

Sebastian Redl
Sebastian Redl

Reputation: 72019

Observe the types of your snippet.

let m = data.deploy_info.value // value is a Vec<MyAppDeployInfo>
    .iter() // returns a Iterator<Item=&MyAppDeployInfo>
    .filter(|&x| x.app == "myapp")
    .collect::<Vec<_>>(); // collects into a Vec<&MyAppDeployInfo>

if m.len() > 1 {
    return Err(Error::MultipleDeployInfo);
}

match m.get(0) { // get() returns a reference to an element
                 // i.e. a &&MyAppDeployInfo
        Some(&x) // pattern match says x : &MyAppDeployInfo
            => Ok(x), // which matches the return type
                      // but you get a borrowing error.
        None => Err(Error::NoDeployInfo),
    }
}

Now if you change the return type to Result<MyAppDeployInfo, Error> as you should, you get the mismatched type issue, because x is a reference. If you dereference x, you get the error "cannot move out of borrowed content", because MyAppDeployInfo is not Copy, and you're trying to move. If you write x.clone(), it should work, unless you changed something else?

Alternatively, you can, from the beginning, work with moving content around. If you write data.deploy_info.value.into_iter().filter(|x| x.app == "myapp"), you move out of the initial structure instead of copying it. Then the resulting Vec will have MyAppDeployInfo as its item type. Then you could make it mut and use pop() instead of get(0) to get the only element in a way that you can move out of.

Or you can do what @ker recommended and not use collect() in the first place. I'd still switch to into_iter() though, making this the final code:

fn terraform_deploy_info(app: &MyApp) // no explicit lifetime needed
        -> std::result::Result<MyAppDeployInfo, Error> {
    let data = // ...

    let mut m = data.deploy_info.value.into_iter()
        .filter(|x| x.app == "myapp").fuse();

    match (m.next(), m.next()) {
        (None, None) => Err(Error::NoDeployInfo),
        (Some(x), None) => Ok(x),
        (Some(_), Some(_)) => Err(Error::MultipleDeployInfo),
        (None, Some(_)) => panic!("Iterator::fuse broken"),
    }
}

Upvotes: 2

Related Questions