Wavefront 3D Obj parser/viewer For learning Rust I decided to make a 3D .obj viewer. I wrote a simplified OBJ parser without materials that only supports triangles.

Considering I'm completely new to Rust and don't really know anyone who's good at idiomatic Rust I figured I'd ask here :)

use std::fs;

use crate::geometry::*;
use crate::object::Object;

fn str_to_vec3(s: &amp;str) -&gt; Option&lt;Vec3&gt; {
    let bits: Vec&lt;&amp;str&gt; = s.split(" ").collect();
    if bits.len() != 3 {
        return None;
    }

    Some(Vec3 {
        x: bits[0].parse().unwrap(),
        y: bits[1].parse().unwrap(),
        z: bits[2].parse().unwrap(),
    })
}

fn str_to_vec2(s: &amp;str) -&gt; Option&lt;Vec2&gt; {
    let bits: Vec&lt;&amp;str&gt; = s.split(" ").collect();
    if bits.len() != 2 {
        return None;
    }

    Some(Vec2 {
        x: bits[0].parse().unwrap(),
        y: bits[1].parse().unwrap(),
    })
}

type VertexDescription = (usize, usize, usize);
type TriangleDescription = (VertexDescription, VertexDescription, VertexDescription);

fn str_to_vertex_description(s: &amp;str) -&gt; Option&lt;VertexDescription&gt; {
    let bits: Vec&lt;&amp;str&gt; = s.split("/").collect();
    if bits.len() != 3 {
        return None;
    }

    Some((
        bits[0].parse::&lt;usize&gt;().unwrap() - 1,
        bits[1].parse::&lt;usize&gt;().unwrap() - 1,
        bits[2].parse::&lt;usize&gt;().unwrap() - 1,
    ))
}

fn str_to_triangle_description(s: &amp;str) -&gt; Option&lt;TriangleDescription&gt; {
    let bits: Vec&lt;&amp;str&gt; = s.split(" ").collect();
    if bits.len() != 3 {
        return None;
    }

    Some((
        str_to_vertex_description(bits[0]).expect("Failed to parse vertex"),
        str_to_vertex_description(bits[1]).expect("Failed to parse vertex"),
        str_to_vertex_description(bits[2]).expect("Failed to parse vertex"),
    ))
}

pub fn parse(path: String) -&gt; Object {
    let object_string = fs::read_to_string(path).expect("Cannot read .obj file!");
    let lines = object_string.lines();
    let mut positions: Vec&lt;Vec3&gt; = vec![];
    let mut normals: Vec&lt;Vec3&gt; = vec![];
    let mut uvs: Vec&lt;Vec2&gt; = vec![];
    let mut triangle_descriptions: Vec&lt;TriangleDescription&gt; = vec![];

    for line in lines {
        match &amp;line[0..2] {
            "v " =&gt; match str_to_vec3(&amp;line[2..]) {
                Some(vec3) =&gt; positions.push(vec3),
                None =&gt; panic!("Malformed 'v' value!"),
            },
            "vn" =&gt; match str_to_vec3(&amp;line[3..]) {
                Some(vec3) =&gt; normals.push(vec3),
                None =&gt; panic!("Malformed 'vn' value!"),
            },
            "vt" =&gt; match str_to_vec2(&amp;line[3..]) {
                Some(vec2) =&gt; uvs.push(vec2),
                None =&gt; panic!("Malformed 'vt' value!"),
            },
            "f " =&gt; match str_to_triangle_description(&amp;line[2..]) {
                Some(td) =&gt; triangle_descriptions.push(td),
                None =&gt; panic!("Malformed 'f' value!"),
            },
            _ =&gt; {}
        }
    }

    let triangles: Vec&lt;Triangle3&gt; = triangle_descriptions
        .iter()
        .map(|&amp;td| {
            let v1: Vertex3 = Vertex3 {
                pos: positions[(td.0).0],
                uv: uvs[(td.0).1],
                normal: normals[(td.0).2],
            };
            let v2: Vertex3 = Vertex3 {
                pos: positions[(td.1).0],
                uv: uvs[(td.1).1],
                normal: normals[(td.1).2],
            };
            let v3: Vertex3 = Vertex3 {
                pos: positions[(td.2).0],
                uv: uvs[(td.2).1],
                normal: normals[(td.2).2],
            };
            (v1, v2, v3)
        })
        .collect();

    let mut obj = Object::new();
    obj.triangles = triangles;
    obj
}


Is this a good approach? What can I improve on?

Any feedback is welcome!
fn str_to_vec3(s: &amp;str) -&gt; Option&lt;Vec3&gt; {
    let bits: Vec&lt;&amp;str&gt; = s.split(" ").collect();
    if bits.len() != 3 {
        return None;
    }

    Some(Vec3 {
        x: bits[0].parse().unwrap(),
        y: bits[1].parse().unwrap(),
        z: bits[2].parse().unwrap(),
    })

}


For some errors, you panic (if parsing fails) and for other errors you return None. You really should pick one strategy and stick with it. If you want to avoid panicing it is more idiomatic to return Result than Option when it is an error.

Much of your code repeats the same basic action: split/parse/return. You can combine some of the code with a function like this:

fn split_parse&lt;T: std::str::FromStr&gt;(text: &amp;str, split: &amp;str) -&gt; Result&lt;Vec&lt;T&gt;, T::Err&gt; {
    text.split(split).map(str::parse).collect()
}