Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to derive UnpackValue for a struct #141

Open
Timmmm opened this issue Feb 3, 2025 · 2 comments
Open

How to derive UnpackValue for a struct #141

Timmmm opened this issue Feb 3, 2025 · 2 comments

Comments

@Timmmm
Copy link

Timmmm commented Feb 3, 2025

Hi, I'm considering using Starlark as a config file format - where you might normally use JSON or YAML or TOML via Serde.

So for example instead of a config file like

{
  "servers": [
    {
     "name": "foo",
     "url": "foo.example.com",
     "port": 80
     },
    {
     "name": "bar",
     "url": "bar.example.com",
     "port": 80
     },
  ]
}

You could have

def server(name):
    return {
        "name": name,
        "url": name + ".example.com",
        "port": 80,
    }

{
    "servers": [server("foo"), server("bar")],
}

I got as far as this:

use std::path::Path;

use allocative::Allocative;
use starlark::any::ProvidesStaticType;
use starlark::environment::Globals;
use starlark::environment::Module;
use starlark::eval::Evaluator;
use starlark::syntax::AstModule;
use starlark::syntax::Dialect;
use starlark::values::starlark_value;
use starlark::values::NoSerialize;
use starlark::values::StarlarkValue;
use starlark::values::UnpackValue;

use anyhow::{anyhow, Result};

#[derive(
    Debug,
    derive_more::Display,
    Allocative,
    NoSerialize,
    ProvidesStaticType,
    UnpackValue,
)]
struct Config {
    test: String,
}

#[starlark_value(type = "Config", UnpackValue, StarlarkTypeRepr)]
impl<'v> StarlarkValue<'v> for Config {}

pub fn read_config(path: &Path) -> Result<Config> {
    let content = std::fs::read_to_string(path)?;

    let file_name = path.file_name().ok_or(anyhow!("No file name in path"))?;
    let file_name = file_name.to_str().ok_or(anyhow!("File name isn't valid UTF-8"))?;

    read_config_text(content, file_name)
}

pub fn read_config_text(content: String, file_name: &str) -> Result<Config> {
    let dialect = Dialect {
        enable_f_strings: true,
        ..Dialect::Extended
    };

    let ast = AstModule::parse(file_name, content, &dialect).map_err(|e| anyhow!("Parse error: {e}"))?;

    let globals = Globals::standard();
    let module = Module::new();
    let mut eval = Evaluator::new(&module);
    let value = eval.eval_module(ast, &globals).map_err(|e| anyhow!("Eval error: {e}"))?;
    UnpackValue::unpack_value_err(value)
}

Unfortunately #[derive(UnpackValue)] apparently only works for enums. Is this kind of use case supported?

@ndmitchell
Copy link
Contributor

You can write your own UnpackValue by hand, which shouldn't be too difficult.

I do agree that a reasonable UnpackValue implementation for a struct would be to UnpackValue all the fields from appropriately named Dict (or maybe anything with a get_item). I think we'd take a patch adding that functionality to UnpackValue.

Your broad use case makes a lot of sense, and we've seen many people do things like that. I would say the reason they haven't wanted exactly UnpackValue the way you are after is because often they'll define a data type Server and a function server which is exposed as a Rust function in the Starlark environment, so instead of users producing a Dict, they'd have to call server. Either can work though.

@Timmmm
Copy link
Author

Timmmm commented Feb 3, 2025

Hmm maybe I can just implement serde::Deserialise for Value? I will try...

they'll define a data type Server and a function server which is exposed as a Rust function in the Starlark environment, so instead of users producing a Dict, they'd have to call server.

Yeah I guess that's the imperative vs functional style. I think I prefer functional in this case, even though Python syntax is not very functional - especially as it matches how you'd do it in JSON/YAML/etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants