How can I turn a HashMap into a string in rust?
Worst case scenario, if it is not possible to stringify a HashMap (could be done looping through the keys and values but it is too much)... How can I turn it into a JSON?
In my case I have {9a6743c2-7c49-4c4d-a136-44114ca50246: "Larry", 9a6743c2-7c49-4c4d-a136-44114ca50236: "Farry"} as my hash map... two of to fields, entries{id: name}. I wanna send that to a client, so I could JSON-ify it or String-if it.
You can use the serde_json crate for this purpose.
use std::collections::HashMap;
fn main() {
let mut data: HashMap<&'static str, i32> = HashMap::new();
data.insert("purpose", 42);
data.insert("nice", 69);
let serialized_data = serde_json::to_string(&data).unwrap();
println!("{}", serialized_data);
}
{"nice":69,"purpose":42}
Related
I'm writing a program that handles a vector which is combination of numbers and letters (hence Vec<String>). I sort it with the .sort() method and am now trying to write it to a file.
Where strvec is my sorted vector that I'm trying to write using std::fs::write;
println!("Save results to file?");
let to_save: String = read!();
match to_save.as_str() {
"y" => {
println!("Enter filename");
let filename: String = read!();
let pwd = current_dir().into();
write("/home/user/dl/results", strvec);
Rust tells me "the trait AsRef<[u8]> is not implemented for Vec<String>". I've also tried using &strvec.
How do I avoid this/fix it?
When it comes to writing objects to the file you might want to consider serialization. Most common library for this in Rust is serde, however in this example where you want to store vector of Strings and if you don't need anything human readable in file (but it comes with small size :P), you can also use bincode:
use std::fs;
use bincode;
fn main() {
let v = vec![String::from("aaa"), String::from("bbb")];
let encoded_v = bincode::serialize(&v).expect("Could not encode vector");
fs::write("file", encoded_v).expect("Could not write file");
let read_v = fs::read("file").expect("Could not read file");
let decoded_v: Vec<String> = bincode::deserialize(&read_v).expect("Could not decode vector");
println!("{:?}", decoded_v);
}
Remember to add bincode = "1.3.3" under dependencies in Cargo.toml
#EDIT:
Actually you can easily save String to the file so simple join() should do:
use std::fs;
fn main() {
let v = vec![
String::from("aaa"),
String::from("bbb"),
String::from("ccc")];
fs::write("file", v.join("\n")).expect("");
}
Rust can't write anything besides a &[u8] to a file. There are too many different ways which data can be interpreted before it gets flattened, so you need to handle all of that ahead of time. For a Vec<String>, it's pretty simple, and you can just use concat to squish everything down to a single String, which can be interpreted as a &[u8] because of its AsRef<u8> trait impl.
Another option would be to use join, in case you wanted to add some sort of delimiter between your strings, like a space, comma, or something.
fn main() {
let strvec = vec![
"hello".to_string(),
"world".to_string(),
];
// "helloworld"
std::fs::write("/tmp/example", strvec.concat()).expect("failed to write to file");
// "hello world"
std::fs::write("/tmp/example", strvec.join(" ")).expect("failed to write to file");
}
You can't get a &[u8] from a Vec<String> without copying since a slice must refer to a contiguous sequence of items. Each String will have its own allocation on the heap somewhere, so while each individual String can be converted to a &[u8], you can't convert the whole vector to a single &[u8].
While you can .collect() the vector into a single String and then get a &[u8] from that, this does some unnecessary copying. Consider instead just iterating the Strings and writing each one to the file. With this helper, it's no more complex than using std::fs::write():
use std::path::Path;
use std::fs::File;
use std::io::Write;
fn write_each(
path: impl AsRef<Path>,
items: impl IntoIterator<Item=impl AsRef<[u8]>>,
) -> std::io::Result<()> {
let mut file = File::create(path)?;
for i in items {
file.write_all(i.as_ref())?;
}
// Surface any I/O errors that could otherwise be swallowed when
// the file is closed implicitly by being dropped.
file.sync_all()
}
The bound impl IntoIterator<Item=impl AsRef<[u8]>> is satisfied by both Vec<String> and by &Vec<String>, so you can call this as either write_each("path/to/output", strvec) (to consume the vector) or write_each("path/to/output", &strvec) (if you need to hold on to the vector for later).
I need to create packet to send to the server. For this purpose I use vector with byteorder crate. When I try to append string, Rust compiler tells I use unsafe function and give me an error.
use byteorder::{LittleEndian, WriteBytesExt};
fn main () {
let login = "test";
let packet_length = 30 + (login.len() as i16);
let mut packet = Vec::new();
packet.write_u8(0x00);
packet.write_i16::<LittleEndian>(packet_length);
packet.append(&mut Vec::from(String::from("game name ").as_bytes_mut()));
// ... rest code
}
The error is:
packet.append(&mut Vec::from(String::from("game name ").as_bytes_mut()));
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call to unsafe function
This is playground to reproduce: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=381c6d14660d47beaece15d068b3dc6a
What is the correct way to insert some string as bytes into vector ?
The unsafe function called was as_bytes_mut(). This creates a mutable reference with exclusive access to the bytes representing the string, allowing you to modify them. You do not really need a mutable reference in this case, as_bytes() would have sufficed.
However, there is a more idiomatic way. Vec<u8> also functions as a writer (it implements std::io::Write), so you can use one of its methods, or even the write! macro, to write encoded text on it.
use std::io::Write;
use byteorder::{LittleEndian, WriteBytesExt};
fn main () -> Result<(), std::io::Error> {
let login = "test";
let packet_length = 30 + (login.len() as i16);
let mut packet = Vec::new();
packet.write_u8(0x00)?;
packet.write_i16::<LittleEndian>(packet_length)?;
let game_name = String::from("game name");
write!(packet, "{} ", game_name)?;
Ok(())
}
Playground
See also:
Use write! macro with a string instead of a string literal
What's the de-facto way of reading and writing files in Rust 1.x?
You can use .extend() on the Vec and pass in the bytes representation of the String:
use byteorder::{LittleEndian, WriteBytesExt};
fn main() {
let login = "test";
let packet_length = 30 + (login.len() as i16);
let mut packet = Vec::new();
packet.write_u8(0x00);
packet.write_i16::<LittleEndian>(packet_length);
let string = String::from("game name ");
packet.extend(string.as_bytes());
}
Playground
I'd like to read some json into a static HashMap, and am using lazy_static and serde, but I can't figure out how (if at all) I can fix this serde lifetime issue:
#[macro_use]
extern crate lazy_static;
use std::fs::File;
use std::io::BufReader;
use std::collections::HashMap;
lazy_static! {
static ref KEYWORDS: HashMap<&'static str, i32> = {
let file = File::open("words.json").unwrap();
let reader = BufReader::new(file);
serde_json::from_reader(reader).unwrap()
};
}
playground link
error: implementation of serde::de::Deserialize is not general enough
note: HashMap<&str, i32> must implement serde::de::Deserialize<'0>, for any lifetime '0
note: but HashMap<&str, i32> actually implements serde::de::Deserialize<'1>, for some specific lifetime '1
words.json is a simple json map: {"aaargh": 1}.
I'm open to another, non-lazy_static approach if need be.
When using serde_json::from_str to deserialize from &str ⟶ HashMap<&str, i32>, the input JSON string needs to outlive the string slices in the output. This is the role of the 'a lifetime in the signature: https://docs.rs/serde_json/1.0.40/serde_json/fn.from_str.html
That means if the output needs to contain string slices with 'static lifetime, the input JSON data must also have 'static lifetime. We know how to do that -- lazy_static!
use lazy_static::lazy_static;
use std::collections::HashMap;
lazy_static! {
static ref KEYWORDS: HashMap<&'static str, i32> = {
lazy_static! {
static ref WORDS_JSON: String = {
std::fs::read_to_string("words.json").unwrap()
};
}
serde_json::from_str(&WORDS_JSON).unwrap()
};
}
The error message is telling you that you can't deserialize to a &'static str. As the deserializer goes along creating the entries, the &str keys could only have as long a lifetime as a borrow of the buffer the deserializer is reading the file into. But &'static str must point to a str which lives forever.
I see two solutions here: The easy way and the hard way.
The easy way: Just change &'static str in the type to String and it compiles. This way the HashMap owns the keys; serde already knows how to deserialize owned strings.
static ref KEYWORDS: HashMap<String, i32> = { // ...
The hard way: Technically you can still get your HashMap<&'static str, i32> by leaking the backing buffers of the Strings. Normally "leaking" is bad, but since this is a lazy static, it really makes no difference as those buffers would never be freed anyways. Getting a &'static str by leaking a String looks like this:
fn leak_string(from: String) -> &'static str {
Box::leak(from.into_boxed_str())
}
The problem is that serde doesn't do this automatically. One way to accomplish this would be to deserialize to the HashMap<String, i32> first and then convert it to HashMap<&'static string, i32> by taking each of the entries and inserting them into a new HashMap after running the keys through leak_string. This is inefficient as there was no need to collect into a HashMap in the first place. A better solution would involve writing a custom deserializer that did leak_string "on the fly". Since the easy way is so much easier, and there's some stumbling blocks for this hard way, I don't think it's useful to provide a full code sample here.
The only real advantage of "the hard way" vs "the easy way" is that "the hard way" requires one pointer's worth less memory for each key in the HashMap (&str is pointer+len; String is pointer+len+capacity). It's also nice in that it doesn't change your type signature, but there's very little you can do with a &'static str that you can't do with a String.
The culprit here is how serde_json::from_reader is defined. From its documentation:
pub fn from_reader<R, T>(rdr: R) -> Result<T> where
R: Read,
T: DeserializeOwned,
So, the result must be owned data, not borrowed. Even the &'static won't do. You have to use String here:
lazy_static! {
static ref KEYWORDS: HashMap<String, i32> = {
let file = File::open("words.json").unwrap();
let reader = BufReader::new(file);
serde_json::from_reader(reader).unwrap()
};
}
I want to make a JSON object which includes multiple types. Here's the structure:
{
"key1": "value",
"key2": ["val", "val", "val"]
"key3": { "keyX": 12 }
}
How can I make a HashMap which accepts all these types?
I'm trying this:
let item = HashMap::new();
item.insert("key1", someString); //type is &str
item.insert("key2", someVecOfStrings); //type is Vec<String>
item.insert("key3", someOtherHashMap); //Type is HashMap<&str, u32>
let response = json::encode(&item).unwrap();
I know that the hash map does not have enough type info, but I'm not sure how I can make it work. I have tried setting an explicit type on item which was HashMap<&str, Encodable> but then it's just another error. What is the correct way to do this?
You should use an enum type as value in your HashMap. That enum needs to have a variant for each possible type (boolean, number, string, list, map...) and an associated value of appropriate type for each variant:
enum JsonValue<'a> {
String(&'a str),
VecOfString(Vec<String>),
AnotherHashMap(HashMap<&'a str, u32>),
}
Fortunately, there already is an implementation of a JSON value type, part of the serde_json crate which is built on the serde crate.
Here is how your code would look if you used the serde_json crate:
extern crate serde_json;
use serde_json::{Value, Map, Number};
fn main() {
let mut inner_map = Map::new();
inner_map.insert("x".to_string(), Value::Number(Number::from(10u64)));
inner_map.insert("y".to_string(), Value::Number(Number::from(20u64)));
let mut map = Map::new();
map.insert("key1".to_string(), Value::String("test".to_string()));
map.insert(
"key2".to_string(),
Value::Array(vec![
Value::String("a".to_string()),
Value::String("b".to_string()),
]),
);
map.insert("key3".to_string(), Value::Object(inner_map));
println!("{}", serde_json::to_string(&map).unwrap());
// => {"key1":"test","key2":["a","b"],"key3":{"x":10,"y":20}}
}
Here is another approach that may be more palatable to you. The serde_json crate provides a way to construct serde_json::Value objects from JSON literals. Your example would look like this:
use serde_json::json;
fn main() {
let item = json!({
"key1": "value",
"key2": ["val", "val", "val"],
"key3": { "keyX": 12 }
});
let response = serde_json::to_string(&item).unwrap();
}
How would I idiomatically create a string to string hashmap in rust. The following works, but is it the right way to do it? is there a different kind of string I should be using?
use std::collections::hashmap::HashMap;
//use std::str;
fn main() {
let mut mymap = HashMap::new();
mymap.insert("foo".to_string(), "bar".to_string());
println!("{0}", mymap["foo".to_string()]);
}
Assuming you would like the flexibility of String, HashMap<String, String> is correct. The other choice is &str, but that imposes significant restrictions on how the HashMap can be used/where it can be passed around; but if it it works, changing one or both parameter to &str will be more efficient. This choice should be dictated by what sort of ownership semantics you need, and how dynamic the strings are, see this answer and the strings guide for more.
BTW, searching a HashMap<String, ...> with a String can be expensive: if you don't already have one, it requires allocating a new String. We have a work around in the form of find_equiv, which allows you to pass a string literal (and, more generally, any &str) without allocating a new String:
use std::collections::HashMap;
fn main() {
let mut mymap = HashMap::new();
mymap.insert("foo".to_string(), "bar".to_string());
println!("{}", mymap.find_equiv(&"foo"));
println!("{}", mymap.find_equiv(&"not there"));
}
playpen (note I've left the Option in the return value, one could call .unwrap() or handle a missing key properly).
Another slightly different option (more general in some circumstances, less in others), is the std::string::as_string function, which allows viewing the data in &str as if it were a &String, without allocating (as the name suggests). It returns an object that can be dereferenced to a String, e.g.
use std::collections::HashMap;
use std::string;
fn main() {
let mut mymap = HashMap::new();
mymap.insert("foo".to_string(), "bar".to_string());
println!("{}", mymap[*string::as_string("foo")]);
}
playpen
(There is a similar std::vec::as_vec.)
Writing this answer for future readers. huon's answer is correct at the time but *_equiv methods were purged some time ago.
The HashMap documentation provides an example on using String-String hashmaps, where &str can be used.
The following code will work just fine. No new String allocation necessary:
use std::collections::HashMap;
fn main() {
let mut mymap = HashMap::new();
mymap.insert("foo".to_string(), "bar".to_string());
println!("{0}", mymap["foo"]);
println!("{0}", mymap.get("foo").unwrap());
}