master
Ondřej Hruška 4 years ago
parent be70c7f497
commit c4148271df
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 28
      Cargo.lock
  2. 2
      Cargo.toml
  3. 13
      yopa/src/cool.rs
  4. 34
      yopa/src/data.rs
  5. 48
      yopa/src/id.rs
  6. 27
      yopa/src/insert.rs
  7. 96
      yopa/src/lib.rs
  8. 19
      yopa/src/model.rs

28
Cargo.lock generated

@ -18,6 +18,17 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "getrandom"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.9" version = "0.1.9"
@ -229,6 +240,22 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom",
"serde",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -263,6 +290,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
"uuid",
] ]
[[package]] [[package]]

@ -10,7 +10,7 @@ edition = "2018"
log = "0.4.13" log = "0.4.13"
simple-logging = "2.0.2" simple-logging = "2.0.2"
yopa = { path = "./yopa" } yopa = { path = "./yopa", features = [ "uuid-ids" ] }
serde_json = "1.0.61" serde_json = "1.0.61"
serde = { version = "1.0.120", features = ["derive"] } serde = { version = "1.0.120", features = ["derive"] }

@ -1,7 +1,10 @@
use std::hash::Hash; //! Utilities for internal use and other cool stuff
use std::collections::HashMap; use std::collections::HashMap;
use std::hash::Hash;
pub fn map_drain_filter<K : Eq + Hash, V>(map : &mut HashMap<K, V>, filter : impl Fn(&K, &V) -> bool) -> Vec<(K, V)> { /// drain_filter() implemented for HashMap. It returns the removed items as a Vec
pub fn map_drain_filter<K: Eq + Hash, V>(map: &mut HashMap<K, V>, filter: impl Fn(&K, &V) -> bool) -> Vec<(K, V)> {
let mut removed = vec![]; let mut removed = vec![];
let mut retain = vec![]; let mut retain = vec![];
for (k, v) in map.drain() { for (k, v) in map.drain() {
@ -15,12 +18,16 @@ pub fn map_drain_filter<K : Eq + Hash, V>(map : &mut HashMap<K, V>, filter : imp
removed removed
} }
/// Get the first or second item from a Vec of (Key, Value) pairs.
/// Use when only one part of the pair is needed
pub trait KVVecToKeysOrValues<K, V> { pub trait KVVecToKeysOrValues<K, V> {
/// Get the first item of each tuple
fn keys(self) -> Vec<K>; fn keys(self) -> Vec<K>;
/// Get the second item of each tuple
fn values(self) -> Vec<V>; fn values(self) -> Vec<V>;
} }
impl<K, V> KVVecToKeysOrValues<K, V> for Vec<(K,V)> { impl<K, V> KVVecToKeysOrValues<K, V> for Vec<(K, V)> {
fn keys(self) -> Vec<K> { fn keys(self) -> Vec<K> {
self.into_iter().map(|(k, _v)| k).collect() self.into_iter().map(|(k, _v)| k).collect()
} }

@ -1,14 +1,14 @@
//! Data value structs //! Data value structs
use serde::{Serialize, Deserialize};
use crate::ID;
use std::borrow::Cow; use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use crate::ID;
use crate::model::DataType; use crate::model::DataType;
/// Value of a particular type /// Value of a particular type
#[derive(Debug,Clone,Serialize,Deserialize,PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum TypedValue { pub enum TypedValue {
/// Text /// Text
String(Cow<'static, str>), String(Cow<'static, str>),
@ -22,7 +22,7 @@ pub enum TypedValue {
impl TypedValue { impl TypedValue {
/// Try ot cast to another type. On error, the original value is returned as Err. /// Try ot cast to another type. On error, the original value is returned as Err.
pub fn cast_to(self, ty : DataType) -> Result<TypedValue, TypedValue> { pub fn cast_to(self, ty: DataType) -> Result<TypedValue, TypedValue> {
match (self, ty) { match (self, ty) {
// to string // to string
(s @ TypedValue::String(_), DataType::String) => Ok(s), (s @ TypedValue::String(_), DataType::String) => Ok(s),
@ -35,7 +35,7 @@ impl TypedValue {
Ok(i) => Ok(TypedValue::Integer(i)), Ok(i) => Ok(TypedValue::Integer(i)),
Err(_) => Err(TypedValue::String(s)) Err(_) => Err(TypedValue::String(s))
} }
}, }
(s @ TypedValue::Integer(_), DataType::Integer) => Ok(s), (s @ TypedValue::Integer(_), DataType::Integer) => Ok(s),
(TypedValue::Decimal(f), DataType::Integer) => Ok(TypedValue::Integer(f.round() as i64)), (TypedValue::Decimal(f), DataType::Integer) => Ok(TypedValue::Integer(f.round() as i64)),
(TypedValue::Boolean(b), DataType::Integer) => Ok(TypedValue::Integer(if b { 1 } else { 0 })), (TypedValue::Boolean(b), DataType::Integer) => Ok(TypedValue::Integer(if b { 1 } else { 0 })),
@ -45,7 +45,7 @@ impl TypedValue {
Ok(i) => Ok(TypedValue::Decimal(i)), Ok(i) => Ok(TypedValue::Decimal(i)),
Err(_) => Err(TypedValue::String(s)) Err(_) => Err(TypedValue::String(s))
} }
}, }
(TypedValue::Integer(i), DataType::Decimal) => Ok(TypedValue::Decimal(i as f64)), (TypedValue::Integer(i), DataType::Decimal) => Ok(TypedValue::Decimal(i as f64)),
(d @ TypedValue::Decimal(_), DataType::Decimal) => Ok(d), (d @ TypedValue::Decimal(_), DataType::Decimal) => Ok(d),
(e @ TypedValue::Boolean(_), DataType::Decimal) => Err(e), (e @ TypedValue::Boolean(_), DataType::Decimal) => Err(e),
@ -62,7 +62,7 @@ impl TypedValue {
Err(TypedValue::String(s)) Err(TypedValue::String(s))
} }
} }
}, }
(TypedValue::Integer(i), DataType::Boolean) => Ok(TypedValue::Boolean(i != 0)), (TypedValue::Integer(i), DataType::Boolean) => Ok(TypedValue::Boolean(i != 0)),
(e @ TypedValue::Decimal(_), DataType::Boolean) => Err(e), (e @ TypedValue::Decimal(_), DataType::Boolean) => Err(e),
(b @ TypedValue::Boolean(_), DataType::Boolean) => Ok(b), (b @ TypedValue::Boolean(_), DataType::Boolean) => Ok(b),
@ -155,36 +155,36 @@ mod tests {
} }
/// Instance of an object /// Instance of an object
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Object { pub struct Object {
/// PK /// PK
pub id : ID, pub id: ID,
/// Object template ID /// Object template ID
pub model_id: ID, pub model_id: ID,
} }
/// Relation between two objects /// Relation between two objects
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Relation { pub struct Relation {
/// PK /// PK
pub id : ID, pub id: ID,
/// Source object ID /// Source object ID
pub object_id : ID, pub object_id: ID,
/// Relation template ID /// Relation template ID
pub model_id: ID, pub model_id: ID,
/// Related object ID /// Related object ID
pub related_id : ID, pub related_id: ID,
} }
/// Value attached to an object /// Value attached to an object
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Value { pub struct Value {
/// PK /// PK
pub id : ID, pub id: ID,
/// Owning object ID /// Owning object ID
pub object_id : ID, pub object_id: ID,
/// Property template ID /// Property template ID
pub model_id: ID, pub model_id: ID,
/// Property value /// Property value
pub value : TypedValue, pub value: TypedValue,
} }

@ -0,0 +1,48 @@
//! IDs used for data objects and models.
//!
//! UUID is always unique; the numeric ID used as a fallback
//! is auto-incrementing, but some values may be skipped.
//!
//! It is better to treat both ID implementations as opaque.
#[cfg(feature = "uuid-ids")]
mod impl_uuid {
/// Common identifier type
#[allow(non_camel_case_types)]
pub type ID = uuid::Uuid;
pub fn next_id() -> ID {
uuid::Uuid::new_v4()
}
pub fn zero_id() -> ID {
uuid::Uuid::nil()
}
}
#[cfg(not(feature = "uuid-ids"))]
mod impl_u64 {
/// Common identifier type
#[allow(non_camel_case_types)]
pub type ID = u64;
lazy_static::lazy_static! {
static ref COUNTER: parking_lot::Mutex<u64> = parking_lot::Mutex::new(0);
}
pub fn next_id() -> ID {
let mut m = COUNTER.lock();
let v = *m;
*m += 1;
v
}
pub fn zero_id() -> ID {
0
}
}
#[cfg(feature = "uuid-ids")]
pub use impl_uuid::{ID, next_id, zero_id};
#[cfg(not(feature = "uuid-ids"))]
pub use impl_u64::{ID, next_id, zero_id};

@ -1,45 +1,46 @@
//! Helper struct for inserting relational data in the database //! Helper struct for inserting relational data in the database
use super::ID; use serde::{Deserialize, Serialize};
use super::data::TypedValue; use super::data::TypedValue;
use serde::{Serialize,Deserialize}; use super::ID;
/// Value to insert /// Value to insert
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsertValue { pub struct InsertValue {
pub model_id: ID, pub model_id: ID,
pub value: TypedValue pub value: TypedValue,
} }
impl InsertValue { impl InsertValue {
pub fn new(model_id : ID, value : TypedValue) -> Self { pub fn new(model_id: ID, value: TypedValue) -> Self {
Self { Self {
model_id, model_id,
value value,
} }
} }
} }
/// Info for inserting a relation /// Info for inserting a relation
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsertRel { pub struct InsertRel {
pub model_id: ID, pub model_id: ID,
pub related_id: ID, pub related_id: ID,
pub values: Vec<InsertValue> pub values: Vec<InsertValue>,
} }
impl InsertRel { impl InsertRel {
pub fn new(model_id : ID, related_id: ID, values : Vec<InsertValue>) -> Self { pub fn new(model_id: ID, related_id: ID, values: Vec<InsertValue>) -> Self {
Self { Self {
model_id, model_id,
related_id, related_id,
values values,
} }
} }
} }
/// Info for inserting a relation /// Info for inserting a relation
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsertObj { pub struct InsertObj {
pub model_id: ID, pub model_id: ID,
pub values: Vec<InsertValue>, pub values: Vec<InsertValue>,
@ -47,11 +48,11 @@ pub struct InsertObj {
} }
impl InsertObj { impl InsertObj {
pub fn new(model_id : ID, values : Vec<InsertValue>, relations: Vec<InsertRel>) -> Self { pub fn new(model_id: ID, values: Vec<InsertValue>, relations: Vec<InsertRel>) -> Self {
Self { Self {
model_id, model_id,
values, values,
relations relations,
} }
} }
} }

@ -1,62 +1,25 @@
use std::collections::{HashMap};
use thiserror::Error;
use crate::cool::{map_drain_filter, KVVecToKeysOrValues};
use model::{ObjectModel};
use insert::InsertObj;
use itertools::Itertools;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap;
use insert::InsertValue; use itertools::Itertools;
use serde::{Deserialize, Serialize};
use thiserror::Error;
mod cool;
#[cfg(feature="uuid-ids")]
pub mod id {
/// Common identifier type
#[allow(non_camel_case_types)]
pub type ID = uuid::Uuid;
pub fn next_id() -> ID {
uuid::Uuid::new_v4()
}
pub fn zero_id() -> ID {
uuid::Uuid::nil()
}
}
#[cfg(not(feature="uuid-ids"))]
pub mod id {
/// Common identifier type
#[allow(non_camel_case_types)]
pub type ID = u64;
lazy_static::lazy_static! {
static ref COUNTER: parking_lot::Mutex<u64> = parking_lot::Mutex::new(0);
}
pub fn next_id() -> ID {
let mut m = COUNTER.lock();
let v = *m;
*m += 1;
v
}
pub fn zero_id() -> ID {
0
}
}
use cool::{KVVecToKeysOrValues, map_drain_filter};
pub use id::ID; pub use id::ID;
use id::next_id; use id::next_id;
use insert::InsertObj;
use insert::InsertValue;
use model::ObjectModel;
pub mod model; pub mod model;
pub mod data; pub mod data;
pub mod insert; pub mod insert;
pub mod id;
mod cool;
#[derive(Debug, Default)] /// Stupid storage with no persistence
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct InMemoryStorage { pub struct InMemoryStorage {
obj_models: HashMap<ID, model::ObjectModel>, obj_models: HashMap<ID, model::ObjectModel>,
rel_models: HashMap<ID, model::RelationModel>, rel_models: HashMap<ID, model::RelationModel>,
@ -67,7 +30,7 @@ pub struct InMemoryStorage {
properties: HashMap<ID, data::Value>, properties: HashMap<ID, data::Value>,
} }
#[derive(Debug,Error)] #[derive(Debug, Error)]
pub enum StorageError { pub enum StorageError {
#[error("Referenced {0} does not exist")] #[error("Referenced {0} does not exist")]
NotExist(Cow<'static, str>), NotExist(Cow<'static, str>),
@ -76,11 +39,13 @@ pub enum StorageError {
} }
impl InMemoryStorage { impl InMemoryStorage {
/// Create empty store
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
pub fn define_object(&mut self, mut tpl : model::ObjectModel) -> Result<ID, StorageError> { /// Define a data object
pub fn define_object(&mut self, mut tpl: model::ObjectModel) -> Result<ID, StorageError> {
if let Some(pid) = tpl.parent_tpl_id { if let Some(pid) = tpl.parent_tpl_id {
if !self.obj_models.contains_key(&pid) { if !self.obj_models.contains_key(&pid) {
return Err(StorageError::NotExist(format!("parent object model {}", pid).into())); return Err(StorageError::NotExist(format!("parent object model {}", pid).into()));
@ -97,6 +62,7 @@ impl InMemoryStorage {
Ok(id) Ok(id)
} }
/// Define a relation between two data objects
pub fn define_relation(&mut self, mut rel: model::RelationModel) -> Result<ID, StorageError> { pub fn define_relation(&mut self, mut rel: model::RelationModel) -> Result<ID, StorageError> {
if !self.obj_models.contains_key(&rel.object_tpl_id) { if !self.obj_models.contains_key(&rel.object_tpl_id) {
return Err(StorageError::NotExist(format!("source object model {}", rel.object_tpl_id).into())); return Err(StorageError::NotExist(format!("source object model {}", rel.object_tpl_id).into()));
@ -116,6 +82,7 @@ impl InMemoryStorage {
Ok(id) Ok(id)
} }
/// Define a property attached to an object or a relation
pub fn define_property(&mut self, mut prop: model::PropertyModel) -> Result<ID, StorageError> { pub fn define_property(&mut self, mut prop: model::PropertyModel) -> Result<ID, StorageError> {
if !self.obj_models.contains_key(&prop.parent_tpl_id) { if !self.obj_models.contains_key(&prop.parent_tpl_id) {
// Maybe it's attached to a relation? // Maybe it's attached to a relation?
@ -143,7 +110,8 @@ impl InMemoryStorage {
Ok(id) Ok(id)
} }
pub fn undefine_object(&mut self, id : ID) -> Result<ObjectModel, StorageError> { /// Delete an object definition and associated data
pub fn undefine_object(&mut self, id: ID) -> Result<ObjectModel, StorageError> {
return if let Some(t) = self.obj_models.remove(&id) { return if let Some(t) = self.obj_models.remove(&id) {
// Remove relation templates // Remove relation templates
let removed_relation_ids = map_drain_filter(&mut self.rel_models, |_k, v| v.object_tpl_id == id || v.related_tpl_id == id) let removed_relation_ids = map_drain_filter(&mut self.rel_models, |_k, v| v.object_tpl_id == id || v.related_tpl_id == id)
@ -167,10 +135,11 @@ impl InMemoryStorage {
Ok(t) Ok(t)
} else { } else {
Err(StorageError::NotExist(format!("object model {}", id).into())) Err(StorageError::NotExist(format!("object model {}", id).into()))
} };
} }
pub fn undefine_relation(&mut self, id : ID) -> Result<model::RelationModel, StorageError> { /// Delete a relation definition and associated data
pub fn undefine_relation(&mut self, id: ID) -> Result<model::RelationModel, StorageError> {
return if let Some(t) = self.rel_models.remove(&id) { return if let Some(t) = self.rel_models.remove(&id) {
// Remove relations // Remove relations
let _ = map_drain_filter(&mut self.relations, |_k, v| v.model_id == id).keys(); let _ = map_drain_filter(&mut self.relations, |_k, v| v.model_id == id).keys();
@ -185,22 +154,23 @@ impl InMemoryStorage {
Ok(t) Ok(t)
} else { } else {
Err(StorageError::NotExist(format!("relation model {}", id).into())) Err(StorageError::NotExist(format!("relation model {}", id).into()))
} };
} }
pub fn undefine_property(&mut self, id : ID) -> Result<model::PropertyModel, StorageError> { /// Delete a property definition and associated data
pub fn undefine_property(&mut self, id: ID) -> Result<model::PropertyModel, StorageError> {
return if let Some(t) = self.prop_models.remove(&id) { return if let Some(t) = self.prop_models.remove(&id) {
// Remove relations // Remove relations
let _ = map_drain_filter(&mut self.properties, |_k, v| v.model_id == id); let _ = map_drain_filter(&mut self.properties, |_k, v| v.model_id == id);
Ok(t) Ok(t)
} else { } else {
Err(StorageError::NotExist(format!("property model {}", id).into())) Err(StorageError::NotExist(format!("property model {}", id).into()))
} };
} }
// DATA // DATA
/// Insert object with relations, validating the data model /// Insert object with relations, validating the data model constraints
pub fn insert_object(&mut self, insobj: InsertObj) -> Result<ID, StorageError> { pub fn insert_object(&mut self, insobj: InsertObj) -> Result<ID, StorageError> {
let obj_model_id = insobj.model_id; let obj_model_id = insobj.model_id;
@ -212,10 +182,10 @@ impl InMemoryStorage {
let object_id = next_id(); let object_id = next_id();
let object = data::Object { let object = data::Object {
id: object_id, id: object_id,
model_id: obj_model_id model_id: obj_model_id,
}; };
let find_values_to_insert = |values : Vec<InsertValue>, parent_id : ID| -> Result<Vec<data::Value>, StorageError> { let find_values_to_insert = |values: Vec<InsertValue>, parent_id: ID| -> Result<Vec<data::Value>, StorageError> {
let mut values_by_id = values.into_iter().into_group_map_by(|iv| iv.model_id); let mut values_by_id = values.into_iter().into_group_map_by(|iv| iv.model_id);
let mut values_to_insert = vec![]; let mut values_to_insert = vec![];
@ -230,7 +200,7 @@ impl InMemoryStorage {
object_id, object_id,
model_id: prop.id, model_id: prop.id,
value: val_instance.value.cast_to(prop.data_type) value: val_instance.value.cast_to(prop.data_type)
.map_err(|v| StorageError::ConstraintViolation(format!("{} cannot accept value {:?}", prop, v).into()))? .map_err(|v| StorageError::ConstraintViolation(format!("{} cannot accept value {:?}", prop, v).into()))?,
}); });
} }
} else { } else {
@ -240,7 +210,7 @@ impl InMemoryStorage {
id: next_id(), id: next_id(),
object_id, object_id,
model_id: prop.id, model_id: prop.id,
value: def.clone() value: def.clone(),
}); });
} else { } else {
return Err(StorageError::ConstraintViolation(format!("{} is required for {} (and no default value is defined)", prop, obj_model).into())); return Err(StorageError::ConstraintViolation(format!("{} is required for {} (and no default value is defined)", prop, obj_model).into()));
@ -278,7 +248,7 @@ impl InMemoryStorage {
id: next_id(), id: next_id(),
object_id, object_id,
model_id: rel_instance.model_id, model_id: rel_instance.model_id,
related_id: rel_instance.related_id related_id: rel_instance.related_id,
}); });
} }
} else { } else {

@ -1,12 +1,13 @@
//! Data model structs and enums //! Data model structs and enums
use serde::{Serialize, Deserialize};
use super::ID;
use super::data::TypedValue;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::fmt; use std::fmt;
use serde::{Deserialize, Serialize};
use super::data::TypedValue;
use super::ID;
/// Get a description of a struct /// Get a description of a struct
pub trait Describe { pub trait Describe {
/// Short but informative description for error messages /// Short but informative description for error messages
@ -14,10 +15,10 @@ pub trait Describe {
} }
/// Object template /// Object template
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObjectModel { pub struct ObjectModel {
/// PK /// PK
pub id : ID, pub id: ID,
/// Template name, unique within the database /// Template name, unique within the database
pub name: String, pub name: String,
/// Parent object template ID /// Parent object template ID
@ -25,7 +26,7 @@ pub struct ObjectModel {
} }
/// Relation between templates /// Relation between templates
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelationModel { pub struct RelationModel {
/// PK /// PK
pub id: ID, pub id: ID,
@ -42,7 +43,7 @@ pub struct RelationModel {
} }
/// Property definition /// Property definition
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PropertyModel { pub struct PropertyModel {
/// PK /// PK
pub id: ID, pub id: ID,
@ -61,7 +62,7 @@ pub struct PropertyModel {
} }
/// Value data type /// Value data type
#[derive(Debug,Clone,Copy,Serialize,Deserialize,Eq,PartialEq)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
pub enum DataType { pub enum DataType {
/// Text /// Text
String, String,

Loading…
Cancel
Save