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

(config): Create a config #6

Open
4 tasks
ArjixWasTaken opened this issue Jan 20, 2023 · 20 comments
Open
4 tasks

(config): Create a config #6

ArjixWasTaken opened this issue Jan 20, 2023 · 20 comments
Assignees

Comments

@ArjixWasTaken
Copy link
Owner

ArjixWasTaken commented Jan 20, 2023

About

A config is really important to the user, because, with it, a user can modify the behavior of this program to suit his/her needs.

Tasks:

  • Make a config module
  • Export the config as a yaml
  • Add a config command to easily modify values in the config file
  • More?
@ArjixWasTaken ArjixWasTaken self-assigned this Jan 20, 2023
@Amanse
Copy link

Amanse commented Feb 10, 2023

i made a similar workflow for my game-rs recently
uses a library to make a simple toml config and a config command which changes config as per needs with FuzzySelector menu
https://github.com/amanse/game-rs

i can make a quick pr, what will be the options in the config?

@ArjixWasTaken
Copy link
Owner Author

ArjixWasTaken commented Feb 10, 2023

That would be wonderful, can you please wait for me to add the config first?
You can exclusively work on the command after that.

Note:

- toml
+ yaml

since it is more user friendly

@Amanse
Copy link

Amanse commented Feb 10, 2023

yess, I'll be waiting
also if i could suggest something, you can use confy, it supports toml, yaml and json and handles the config part with no boilerplate and for all OSes

@ArjixWasTaken
Copy link
Owner Author

I have already written most of the boilerplate code either way, so its ok

@ArjixWasTaken
Copy link
Owner Author

@Amanse here is your plalyground :)

Go wild, I left some comments on how I want this to proceed, but I am open to suggestions.

@ArjixWasTaken
Copy link
Owner Author

ArjixWasTaken commented Feb 11, 2023

One more request that I have is that before applying any change to the config, show a diff and prompt the user if they want to proceed.

@Amanse
Copy link

Amanse commented Feb 12, 2023

nicee that's a great idea, I'll get right on it

@Amanse
Copy link

Amanse commented Feb 12, 2023

Hey, did you have something specific in mind to iterate over the struct fields?
rust doesn't make it easy to dynamically update a field in a struct and serde(_aux) only gives name of field. making static update function for each field will be dumb

i can think of 2 ways it can go:

  • either to Deserialize into a hashmap so we can change with key (selected by the user) [sub-fields will be hashmap inside hasmap]
  • or use something like bevy_reflect which allows changing value with dynamic values but takes away some of the memory safety guarantees of rust

do you have any preference on which way to go or any other way you can think of?

@ArjixWasTaken
Copy link
Owner Author

rust doesn't make it easy to dynamically update a field in a struct and serde(_aux) only gives name of field. making static update function for each field will be dumb

I'll try and make a small prototype for that

@ArjixWasTaken
Copy link
Owner Author

yeah bevy_reflect looks like the way to go, or writing a trait and implementing it for every single structure in the config

actually, the latter sounds more safe and easy, ig ill take the L

@Amanse
Copy link

Amanse commented Feb 13, 2023

yes, the latter is immensely more easy as per my limited experience with bevy_reflect

another way i can think is, since we can get an index and field name with serde safely, we can make a selection to enum mapper and make a function which takes a value and enum and just updates with match
it's the easiest option by far and updating this when config grows will be minimal
but will need a long ass match

trait will be prettier way to code doe, I'll give it a shot next

@Amanse
Copy link

Amanse commented Feb 14, 2023

Ayo check this out
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=06f2842006c70215d1c4a81f780236dd

macros can change dynamically, when we add anything to config we just have to add a macro line, it can even handle different prompts for different types

@ArjixWasTaken
Copy link
Owner Author

Took me some time to learn procedural macros, btw what you linked is a declarative macro

Here I made an example of how this could work

module fake_reflect

Cargo.toml

Toggle
[package]
name = "fake_reflect"
version = "0.1.0"
edition = "2018"

[lib]
proc-macro = true

[dependencies]
syn = "0.15"
quote = "0.6"

lib.rs

Toggle
extern crate proc_macro;
extern crate syn;

#[macro_use]
extern crate quote;

use proc_macro::TokenStream;
use syn::{Data, DataStruct, DeriveInput, Fields, Ident};

fn impl_hello_world(ast: &DeriveInput) -> TokenStream {
    let name = &ast.ident;

    let fields = match &ast.data {
        Data::Struct(DataStruct {
            fields: Fields::Named(fields),
            ..
        }) => &fields.named,
        _ => panic!("expected a struct with named fields"),
    };

    let field_name = fields.iter().map(|field| &field.ident);
    let field_name2 = field_name.clone();
    let field_name3 = field_name.clone();

    let field_type = fields.iter().map(|field| &field.ty);

    let enum_name = syn::parse_str::<Ident>(&format!("{}_field", name.to_string())).unwrap();

    let enum_vars = fields.iter().map(|field| {
        syn::parse_str::<syn::Path>(&format!("{}::{}", enum_name, field.ident.as_ref().unwrap()))
            .unwrap()
    });

    TokenStream::from(quote! {
        #ast

        enum #enum_name {

            #(
                #field_name(#field_type),
            )*
        }

        impl #name {
            fn get(&self, field: impl ToString) -> anyhow::Result<#enum_name> {
                match field.to_string().as_str() {
                    #(
                        stringify!(#field_name2) => Ok(#enum_vars(self.#field_name3)),
                    )*
                    _ => Err(anyhow::anyhow!("Unknown field.")),
                }
            }
        }
    })
}

#[proc_macro_attribute]
pub fn hello_world(args: TokenStream, input: TokenStream) -> TokenStream {
    let _ = args;

    // Parse the string representation
    let ast = syn::parse_macro_input!(input as DeriveInput);

    // Build and return the generated impl
    impl_hello_world(&ast)
}

usage

Cargo.toml

Toggle
...

[dependencies]
fake_reflect = { path = "fake_reflect" }

...

main.rs

Toggle
#[macro_use]
extern crate fake_reflect;

pub fn main() {
    #[fake_reflect::hello_world]
    struct Waffles {
        hello: i16,
    }

    let a = Waffles { hello: 42 };

    match a.get("hello").unwrap() {
        Waffles_field::hello(value) => println!("{}", value),
        _ => (),
    };
}

@ArjixWasTaken
Copy link
Owner Author

Here is the generated code from the above message:

pub fn main() {
    struct Waffles {
        hello: i16,
    }
    enum Waffles_field {
        hello(i16),
    }
    impl Waffles {
        fn get(&self, field: impl ToString) -> anyhow::Result<Waffles_field> {
            match field.to_string().as_str() {
                "hello" => Ok(Waffles_field::hello(self.hello)),
                _ => Err(anyhow::anyhow!("Unknown field.")),
            }
        }
    }
    let a = Waffles { hello: 42 };
    match a.get("hello").unwrap() {
        Waffles_field::hello(value) => println!("{}", value),
        _ => ()
    };
}

PS: I used cargo expand

@Amanse
Copy link

Amanse commented Feb 15, 2023

wow, took me some time to understand but this simplifies the whole process, rust macros are awesome lol
will make a pr asap

@Amanse
Copy link

Amanse commented Feb 16, 2023

kinda under estimated rust borrow checker, String doesn't implement Copy strikes again, it may take some more time than i expected

@ArjixWasTaken
Copy link
Owner Author

kinda under estimated rust borrow checker, String doesn't implement Copy strikes again, it may take some more time than i expected

.clone()

@Amanse
Copy link

Amanse commented Feb 16, 2023

i misunderstood what the error meant, working now, partially added the update method too

@Amanse
Copy link

Amanse commented Feb 16, 2023

Update function working
Only need to loop over fields and call the function for selected field now

@Amanse
Copy link

Amanse commented Feb 21, 2023

sorry i was in hospital for a few days
What i am stuck on is getting new value into proc macro, since field type needs to be iterated
and another method that is working is passing value with field enum that we get from the get method. But we cannot change value inside enum from original to new value without matching with each enum

i was looking into ways of doing it with serde and found this

use-serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize)]
struct ExampleStruct {
    field_a: i32,
    field_b: String,
}

fn main() {
    let mut my_struct = ExampleStruct {
        field_a: 42,
        field_b: String::from("hello"),
    };
    
    let mut field_updates = HashMap::new();
    field_updates.insert("field_a", 99);
    field_updates.insert("field_b", String::from("world"));
    
    for (field_name, field_value) in field_updates {
        let field_name = field_name.to_string();
        let field_value = serde_json::to_value(&field_value).unwrap();
        let mut fields = my_struct.serialize().unwrap();
        fields.remove(&field_name);
        fields.insert(field_name, field_value);
        my_struct = ExampleStruct::deserialize(fields).unwrap();
    }
    
    println!("Updated struct: {:?}", my_struct);
}

bevy_reflect seems a viable option too

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

No branches or pull requests

2 participants