Correct way to create list of structs with conditional values from json?

I'm trying to take a json file (particularly a vscode snippets.json file) file and create a set of structs. The snippet syntax I'm trying to support is a superset of the syntax laid out in the Language Server Protocol, meaning that I some struct attributes have to be instantiated if they aren't present.

The reason being I'm trying to create a drop in replacement for most editor snippet managers, with the capability of having extra features not supported by the current editor, allowing users to easily migrate to using the plugin.

From what I've read, serde is the library I likely need to use, but I can't figure out how to do so. most of the examples I see are directly going from json to struct. this may not actually be what serde is designed for but I wanted to check before attempting to parse manually.

I don't see why you wouldn't want to use serde, which will handle a ton of tedious JSON-to-Rust chores for you automatically and is highly customizable to different input format layouts. For instance, if you need custom defaults for anything not present in the JSON file, you it's easy to specify what they should be. Here's a toy example:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
#[serde(default = "my_default")]
struct MyStruct {
    a: i32,
    b: i32,
    c: i32,
}

fn my_default() -> MyStruct {
    MyStruct { a: 1, b: 2, c: 3 }
}

fn main() {
    use serde_json::from_str;

    let s: MyStruct = from_str(r#"{"a":10, "b":20}"#).unwrap();
    dbg!(s);
}

which outputs:

[src/main.rs:19] s = MyStruct {
    a: 10,
    b: 20,
    c: 3,
}

Or you can specify defaults on a field-by-field basis if that's more convenient.

If you can lay out your data into some mix of primitives, structs and built-in Rust container types, there's almost certainly some way to finesse serde into deserializing it for you with a minimum of hassle.

For instance, for a list of structs, wrapping them in a Vec also works just fine:

    let v: Vec<MyStruct> = from_str(r#"[{"a":5}, {"b":6}]"#).unwrap();
    dbg!(v);

outputs:

[src/main.rs:22] v = [
    MyStruct {
        a: 5,
        b: 2,
        c: 3,
    },
    MyStruct {
        a: 1,
        b: 6,
        c: 3,
    },
]
1 Like

@tuffy
not sure if I should open another question but is there a way to deserialize to a function?

I'm trying to create a trie struct named snippets, that is composed of single character nodes and using another set of structs roughly as terminal characters.
defined below:

#[derive Deserialize, Debug]
pub struct Snippets {
    root: HashMap<char, Node>,
    len: i32,
}
impl Snippets {  
    ...
    pub fn add(&mut self, snip: Snippet) { ....

nodes:

pub struct Node {
    next: HashMap<char, Node>,
    pub value: Option<Snippet>,
}

snippet:

#[derive(Deserialize)]
pub struct Snippet {
    snippet_type: String,
    body: String,
    description: String,
}

here's a snippet of the the contents of the current snippet file:

[else]
trigger = "else"
snippet_type = "conditional statement"
body = ["else:", "\t${1:pass}"]
description = "Code snippet for an else"

["if/else"]
trigger = "if/else"
snippet_type="statement"
body = ["${SNIPPET:if}", "${SNIPPET:else}"]
description = "Code snippet for an if statement with else"

If I could just pass each table to the add function I could use the trigger to build the trie, and use the rest to create the snippet at the end of the trigger. Even if deserialization is impossible in this case, parsing individual tables manually would be more managable.

I saw this SO post on deserializing an array of tables, makes me assume that it's possible to work with muliple tables or multiple nested JSON structures

Note

I wrote a python script to convert the file to toml (funnily enough I thought toml had support in the standard library), so now I'm facing the same issue but with toml

What do you mean by "deserializing to a function"?

It sounds like you want to deserialize the data into some struct then return a closure which closes over the struct and executes some business logic.

Kinda like this:

#[derive(serde::Serialize)]
struct LinearEquation {
  m: f64,
  c: f64,
}

fn deserialize_linear_equation(text: &str) -> Result<impl Fn(f64) -> f64, Error> {
  let state: LinearEquation = serde_json::from_str(text)?;

  Ok(move |x| state.m * x + state.c)
}

You can't deserialize directly to a function because JSON is just data. There's no way for the compiler to know what code your function should contain, so you need to write it yourself.

2 Likes

at the time, I thought maybe the correct course would have been to pass the contents of each table to a function and extract the first variable of the table to a string, and pass the rest to a struct. partially because I was trying to avoid having multiple copies of the same data: if the trigger was used to build the path to the snippet, then why store the trigger in the snippet?

I've been reading more about deserialization and passing all the contents of a table to the snippet struct, then adding it to the trie based off it's trigger is a bit more doable, and an extra string per entry honestly shouldn't have been a major concern in the first place. this issue on toml-rs's github has been especially helpful.

I'm still sort of iffy how instantiating a struct then transferring ownership of that struct to a node will work out, but I don't think that should be too big of a concern given the original variable holding the snippet is meant to be temporary and rust using move semantics for variable assignment.