Hi,
i am trying to find a way to have something similar to SQL Foreign Relationships with Rust Structs.
I have a top Level Configuration struct which holds all configuration, but some of the configuration items need to reference/point to one element of a vector somewhere else in the configuration.
I for multiple reasons, using a actual SQL database is not something i want to do in this situation.
Vector elements are identified by a name String field (i have not used a map for other reasons but would work similarly).
Here is what i currently have which works:
use serde::{Deserialize, Serialize};
// Configuration Struct definitions
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct Config {
pub system_a: SystemA,
pub system_b: SystemB,
pub thing_c: ThingC,
}
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct SystemA {
pub thing_a_list: ThingAList,
}
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct SystemB {
pub thing_b_list: Vec<ThingB>,
}
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct ThingA {
pub name: String,
pub data: String,
}
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct ThingB {
pub name: String,
pub thing_a: ThingAReference,
}
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct ThingC {
pub name: String,
pub thing_a: ThingAReference,
}
// Referencing Definitions
pub trait Referenceable<T> {
fn named_get(&self, name: String) -> T;
fn named_exists(&self, name: String) -> bool;
}
pub type ThingAList = Vec<ThingA>;
impl Referenceable<ThingA> for ThingAList {
fn named_get(&self, name: String) -> ThingA {
let index = self.iter().position(|e| *e.name == name);
match index {
Some(i) => self[i].clone(),
// This is fine since the config always has to validated before commiting
None => panic!("Referenced Thing: '{:?}' does not exist ", name),
}
}
fn named_exists(&self, name: String) -> bool {
let index = self.iter().position(|e| *e.name == name);
index.is_some()
}
}
pub trait References<T> {
fn get_ref(&self, config: Config) -> T;
fn ref_exists(&self, config: Config) -> bool;
}
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
#[serde(from = "String")]
#[serde(into = "String")]
pub struct ThingAReference {
pub name: String,
}
impl Into<String> for ThingAReference {
fn into(self) -> String {
self.name
}
}
impl From<String> for ThingAReference {
fn from(value: String) -> Self {
ThingAReference { name: value }
}
}
impl References<ThingA> for ThingAReference {
fn get_ref(&self, config: Config) -> ThingA {
config.system_a.thing_a_list.named_get(self.clone().into())
}
fn ref_exists(&self, config: Config) -> bool {
config
.system_a
.thing_a_list
.named_exists(self.clone().into())
}
}
fn main() {
println!("Hello, world!");
// Example data
let mut config = Config {
system_a: SystemA {
thing_a_list: Vec::new(),
},
system_b: SystemB {
thing_b_list: Vec::new(),
},
thing_c: ThingC {
name: "name c".to_string(),
thing_a: "thing_a_1".to_string().into(),
},
};
config.system_a.thing_a_list.push(ThingA {
name: "thing_a_1".to_string(),
data: "important data 1".to_string(),
});
config.system_a.thing_a_list.push(ThingA {
name: "thing_a_2".to_string(),
data: "other important data 2".to_string(),
});
config.system_b.thing_b_list.push(ThingB {
name: "thing_b_1".to_string(),
thing_a: "thing_a_1".to_string().into(),
});
// get what thing_b references
println!(
"thing b 0 data from Thing a {:?}",
config.system_b.thing_b_list[0]
.thing_a
.get_ref(config.clone())
.data
);
// get what thing_c references
println!(
"thing c data from Thing a {:?}",
config.thing_c.thing_a.get_ref(config.clone()).data
);
}
In actual code i have these macros so that i don't need to rewrite this for everything that can be referenced or references:
pub trait Referenceable<T> {
fn named_get(&self, name: String) -> T;
fn named_exists(&self, name: String) -> bool;
}
#[macro_export]
macro_rules! impl_referenceable_trait {
($typ:ident, $ele:ty) => {
pub type $typ = Vec<$ele>;
impl Referenceable<$ele> for $typ {
fn named_get(&self, name: String) -> $ele {
let index = self.iter().position(|e| *e.name == name);
match index {
Some(i) => self[i].clone(),
// This is fine since the config always has to validated before commiting
None => panic!("Referenced Thing: '{:?}' does not exist ", name),
}
}
fn named_exists(&self, name: String) -> bool {
let index = self.iter().position(|e| *e.name == name);
index.is_some()
}
}
};
}
pub trait References<T> {
fn get_ref(&self, config: Config) -> T;
fn ref_exists(&self, config: Config) -> bool;
}
#[macro_export]
macro_rules! impl_references_trait {
($thing:ident, $referenced:ty, $( $path:ident ).+) => {
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
#[serde(from = "String")]
#[serde(into = "String")]
pub struct $thing {
pub name: String,
}
impl Into<String> for $thing {
fn into(self) -> String {
self.name
}
}
impl From<String> for $thing {
fn from(value: String) -> Self {
$thing { name: value }
}
}
impl References<$referenced> for $thing {
fn get_ref(&self, config: Config) -> $referenced {
config.$($path).+.named_get(self.clone().into())
}
fn ref_exists(&self, config: Config) -> bool {
config.$($path).+.named_exists(self.clone().into())
}
}
};
}
This works and i am quite happy with it, but there are a few things which i also need but can't quite figure out how to do:
- I need to write validation which checks if all references are valid, which can be done by calling ref_exists on all references, since there are many things that can reference something else in my i code would like to find a way to generate this validation function for the entire config when i use the references macro (or a seperate one) so that i don't forget to validate a reference during validation.
- Something which is Referenceable also needs a function to get all the Things which reference it, i don't need the struct/element itself, i just need its path in the config and name if it has one.