names are now a property and can be chosen arbitrarily
This commit is contained in:
Generated
+71
-1
@@ -650,7 +650,7 @@ dependencies = [
|
||||
"ansi_term",
|
||||
"atty",
|
||||
"bitflags",
|
||||
"strsim",
|
||||
"strsim 0.8.0",
|
||||
"textwrap",
|
||||
"unicode-width",
|
||||
"vec_map",
|
||||
@@ -736,6 +736,41 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11947000d710ff98138229f633039982f0fef2d9a3f546c21d610fee5f8631d5"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae53b4d9cc89c40314ccf2bf9e6ff1eb19c31e3434542445a41893dbf041aec2"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9cd9ac4d50d023af5e710cae1501afb063efcd917bd3fc026e8ed6493cc9755"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.11"
|
||||
@@ -1131,6 +1166,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.1"
|
||||
@@ -1899,6 +1940,28 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "1.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b44be9227e214a0420707c9ca74c2d4991d9955bae9415a8f93f05cebf561be5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_with_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e48b35457e9d855d3dc05ef32a73e0df1e2c0fd72c38796a4ee909160c8eeec2"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha-1"
|
||||
version = "0.8.2"
|
||||
@@ -2059,6 +2122,12 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.0"
|
||||
@@ -2670,6 +2739,7 @@ dependencies = [
|
||||
"rand 0.8.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"simple-logging",
|
||||
"tera",
|
||||
"thiserror",
|
||||
|
||||
@@ -27,6 +27,7 @@ json_dotpath = "1.0.3"
|
||||
anyhow = "1.0.38"
|
||||
thiserror = "1.0.24"
|
||||
clap = "2"
|
||||
serde_with = "1.6.4"
|
||||
|
||||
tokio = { version="0.2.6", features=["full"] }
|
||||
|
||||
|
||||
@@ -86,7 +86,6 @@ export default {
|
||||
return {
|
||||
model: this.object.model, // string is fine
|
||||
id: this.object.id,
|
||||
name: this.name,
|
||||
values,
|
||||
relations,
|
||||
};
|
||||
@@ -140,13 +139,6 @@ export default {
|
||||
<p><input type="button" value="Save" @click="trySave"></p>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="field-name">Name</label></th>
|
||||
<td>
|
||||
<input type="text" id="field-name" v-model="name">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<edit-property v-for="(property, pi) in properties" :model="property" :values="values[property.id]" :key="pi"></edit-property>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -39,17 +39,12 @@ export default {
|
||||
haveRelations: !isEmpty(relations),
|
||||
model_names,
|
||||
values,
|
||||
name: '',
|
||||
relationRefs: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** Get values in the raw format without grouping */
|
||||
collectData() {
|
||||
if (isEmpty(this.name)) {
|
||||
throw new Error("Name is required");
|
||||
}
|
||||
|
||||
let values = [];
|
||||
forEach(objCopy(this.values), (vv, prop_model_id) => {
|
||||
for (let v of vv) {
|
||||
@@ -73,7 +68,6 @@ export default {
|
||||
|
||||
return {
|
||||
model: this.model.id,
|
||||
name: this.name,
|
||||
values,
|
||||
relations,
|
||||
};
|
||||
@@ -127,13 +121,6 @@ export default {
|
||||
<p><input type="button" value="Save" @click="trySave"></p>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="field-name">Name</label></th>
|
||||
<td>
|
||||
<input type="text" id="field-name" v-model="name">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<property v-for="(property, pi) in properties" :model="property" :values="values[property.id]" :key="pi"></property>
|
||||
</table>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,8 +13,13 @@ Define object
|
||||
<h1>Define new object model</h1>
|
||||
|
||||
<form action="/model/object/create" method="POST">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"><br>
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="name">Name:</label></th>
|
||||
<td><input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
@@ -13,8 +13,24 @@ Edit object model
|
||||
<h1>Edit object model {{ model.name }}</h1>
|
||||
|
||||
<form action="/model/object/update/{{ model.id }}" method="POST">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" value="{{ model.name }}" autocomplete="off"><br>
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="name">Name:</label></th>
|
||||
<td><input type="text" id="name" name="name" value="{{ model.name }}" autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="name_property">Name property:</label></th>
|
||||
<td>
|
||||
<select name="name_property" id="name_property" autocomplete="off">
|
||||
<option value=""></option>
|
||||
{% for p in properties %}
|
||||
<option value="{{ p.id }}" {{selected(val=old.name_property, opt=p.id)}}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
@@ -16,28 +16,55 @@ Define property
|
||||
<form action="/model/property/create" method="POST">
|
||||
<input type="hidden" name="object" value="{{object.id}}">
|
||||
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"><br>
|
||||
|
||||
<label for="optional">Optional:</label>
|
||||
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=old.optional)}} autocomplete="off">
|
||||
<br>
|
||||
|
||||
<label for="multiple">Multiple:</label>
|
||||
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=old.multiple)}} autocomplete="off"><br>
|
||||
|
||||
<label for="data_type">Type:</label>
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="name">Name:</label></th>
|
||||
<td><input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="unique">Unique:</label></th>
|
||||
<td><input type="checkbox" name="unique" id="unique" value="true" {{opt(checked=old.unique)}} autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="optional">Optional:</label></th>
|
||||
<td><input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=old.optional)}} autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="multiple">Multiple:</label></th>
|
||||
<td><input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=old.multiple)}} autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="data_type">Type:</label></th>
|
||||
<td>
|
||||
<select name="data_type" id="data_type" autocomplete="off">
|
||||
<option value="String" {{selected(opt="String",val=old.data_type)}}>String</option>
|
||||
<option value="Integer" {{selected(opt="Integer",val=old.data_type)}}>Integer</option>
|
||||
<option value="Decimal" {{selected(opt="Decimal",val=old.data_type)}}>Decimal</option>
|
||||
<option value="Boolean" {{selected(opt="Boolean",val=old.data_type)}}>Boolean</option>
|
||||
</select><br>
|
||||
|
||||
<label for="default">Default:</label>
|
||||
<input type="text" id="default" name="default" value="{{old.default}}" autocomplete="off"><br>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="default">Default:</label></th>
|
||||
<td><input type="text" id="default" name="default" value="{{old.default}}" autocomplete="off"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// multiple and unique are XORed. This is also enforced server-side
|
||||
let multiple = document.getElementById('multiple');
|
||||
let unique = document.getElementById('unique');
|
||||
unique.addEventListener('input', function () {
|
||||
multiple.checked &= !unique.checked;
|
||||
})
|
||||
multiple.addEventListener('input', function () {
|
||||
unique.checked &= !multiple.checked;
|
||||
})
|
||||
})();
|
||||
</script>
|
||||
|
||||
{%- endblock %}
|
||||
|
||||
@@ -13,28 +13,64 @@ Edit property
|
||||
<h1>Edit property {{model.name}}</h1>
|
||||
|
||||
<form action="/model/property/update/{{model.id}}" method="POST">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" value="{{model.name}}" autocomplete="off"><br>
|
||||
|
||||
<label for="optional">Optional:</label>
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="name">Name:</label></th>
|
||||
<td><input type="text" id="name" name="name" value="{{model.name}}" autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="unique">Unique:</label></th>
|
||||
<td>
|
||||
<input type="checkbox" name="unique" id="unique" value="true" {{opt(checked=model.unique)}} autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="optional">Optional:</label></th>
|
||||
<td>
|
||||
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=model.optional)}} autocomplete="off">
|
||||
<br>
|
||||
|
||||
<label for="multiple">Multiple:</label>
|
||||
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=model.multiple)}} autocomplete="off"><br>
|
||||
|
||||
<label for="data_type">Type:</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="multiple">Multiple:</label></th>
|
||||
<td>
|
||||
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=model.multiple)}} autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="data_type">Type:</label></th>
|
||||
<td>
|
||||
<select name="data_type" id="data_type" autocomplete="off">
|
||||
<option value="String" {{selected(val=model.data_type, opt="String")}}>String</option>
|
||||
<option value="Integer" {{selected(val=model.data_type, opt="Integer")}}>Integer</option>
|
||||
<option value="Decimal" {{selected(val=model.data_type, opt="Decimal")}}>Decimal</option>
|
||||
<option value="Boolean" {{selected(val=model.data_type, opt="Boolean")}}>Boolean</option>
|
||||
</select><br>
|
||||
|
||||
<label for="default">Default:</label>
|
||||
<input type="text" id="default" name="default" value="{{model.default | print_typed_value}}" autocomplete="off"><br>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="default">Default:</label></th>
|
||||
<td>
|
||||
<input type="text" id="default" name="default" value="{{model.default | print_typed_value}}" autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// multiple and unique are XORed. This is also enforced server-side
|
||||
let multiple = document.getElementById('multiple');
|
||||
let unique = document.getElementById('unique');
|
||||
unique.addEventListener('input', function () {
|
||||
multiple.checked &= !unique.checked;
|
||||
})
|
||||
multiple.addEventListener('input', function () {
|
||||
unique.checked &= !multiple.checked;
|
||||
})
|
||||
})();
|
||||
</script>
|
||||
|
||||
{%- endblock %}
|
||||
|
||||
@@ -15,25 +15,42 @@ Define relation
|
||||
<form action="/model/relation/create" method="POST">
|
||||
<input type="hidden" name="object" value="{{object.id}}">
|
||||
|
||||
<label for="name">Name:</label>
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="name">Name:</label></th>
|
||||
<td>
|
||||
<input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"><br>
|
||||
|
||||
<label for="reciprocal_name">Reciprocal name:</label>
|
||||
<input type="text" id="reciprocal_name" name="reciprocal_name" value="{{old.reciprocal_name}}" autocomplete="off"><br>
|
||||
|
||||
<label for="optional">Optional:</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="reciprocal_name">Reciprocal name:</label></th>
|
||||
<td>
|
||||
<input type="text" id="reciprocal_name" name="reciprocal_name" value="{{old.reciprocal_name}}" autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="optional">Optional:</label></th>
|
||||
<td>
|
||||
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=old.optional)}} autocomplete="off">
|
||||
<br>
|
||||
|
||||
<label for="multiple">Multiple:</label>
|
||||
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=old.multiple)}} autocomplete="off"><br>
|
||||
|
||||
<label for="related">Related object:</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="multiple">Multiple:</label></th>
|
||||
<td>
|
||||
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=old.multiple)}} autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="related">Related object:</label></th>
|
||||
<td>
|
||||
<select name="related" id="related" autocomplete="off">
|
||||
{% for m in models %}
|
||||
<option value="{{ m.id }}" {{selected(val=old.related, opt=m.id)}}>{{ m.name }}</option>
|
||||
{% endfor %}
|
||||
</select><br>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
@@ -13,18 +13,29 @@ Edit relation
|
||||
<h1>Edit relation model "{{model.name}}"</h1>
|
||||
|
||||
<form action="/model/relation/update/{{model.id}}" method="POST">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" value="{{model.name}}" autocomplete="off"><br>
|
||||
|
||||
<label for="reciprocal_name">Reciprocal name:</label>
|
||||
<input type="text" id="reciprocal_name" name="reciprocal_name" value="{{model.reciprocal_name}}" autocomplete="off"><br>
|
||||
|
||||
<label for="optional">Optional:</label>
|
||||
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=model.optional)}} autocomplete="off">
|
||||
<br>
|
||||
|
||||
<label for="multiple">Multiple:</label>
|
||||
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=model.multiple)}} autocomplete="off"><br>
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="name">Name:</label></th>
|
||||
<td><input type="text" id="name" name="name" value="{{model.name}}" autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="reciprocal_name">Reciprocal name:</label></th>
|
||||
<td><input type="text" id="reciprocal_name" name="reciprocal_name" value="{{model.reciprocal_name}}" autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="optional">Optional:</label>
|
||||
</th>
|
||||
<td><input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=model.optional)}} autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="multiple">Multiple:</label></th>
|
||||
<td><input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=model.multiple)}} autocomplete="off">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>The related object cannot be changed. Create a new relation if needed.</p>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::session_ext::SessionExt;
|
||||
use crate::tera_ext::TeraExt;
|
||||
use crate::utils::{redirect, StorageErrorIntoResponseError};
|
||||
use crate::TERA;
|
||||
use itertools::Itertools;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub(crate) struct ObjectModelDisplay<'a> {
|
||||
@@ -38,6 +39,10 @@ pub(crate) async fn create_form(session: Session) -> actix_web::Result<impl Resp
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ObjectModelForm {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
// #[serde(with="serde_with::rust::default_on_error")] // This is because "" can be selected
|
||||
#[serde(with="my_string_empty_as_none")]
|
||||
pub name_property: Option<ID>,
|
||||
}
|
||||
|
||||
#[post("/model/object/create")]
|
||||
@@ -51,6 +56,7 @@ pub(crate) async fn create(
|
||||
match wg.define_object(ObjectModel {
|
||||
id: Default::default(),
|
||||
name: form.name.clone(),
|
||||
name_property: form.name_property
|
||||
}) {
|
||||
Ok(_id) => {
|
||||
wg.persist().err_to_500()?;
|
||||
@@ -84,12 +90,22 @@ pub(crate) async fn update_form(
|
||||
// Re-fill old values
|
||||
if let Ok(Some(form)) = session.take::<ObjectModelForm>("old") {
|
||||
let mut model = model.clone();
|
||||
context.insert("old", &form);
|
||||
model.name = form.name;
|
||||
model.name_property = form.name_property;
|
||||
context.insert("model", &model);
|
||||
} else {
|
||||
context.insert("model", model);
|
||||
context.insert("old", &ObjectModelForm {
|
||||
name: model.name.to_string(),
|
||||
name_property: model.name_property
|
||||
});
|
||||
}
|
||||
|
||||
let properties = rg.get_property_models_for_parent(*model_id).collect_vec();
|
||||
|
||||
context.insert("properties", &properties);
|
||||
|
||||
TERA.build_response("models/model_update", &context)
|
||||
}
|
||||
|
||||
@@ -107,6 +123,7 @@ pub(crate) async fn update(
|
||||
match wg.update_object_model(ObjectModel {
|
||||
id,
|
||||
name: form.name.clone(),
|
||||
name_property: form.name_property,
|
||||
}) {
|
||||
Ok(_id) => {
|
||||
wg.persist().err_to_500()?;
|
||||
@@ -144,3 +161,89 @@ pub(crate) async fn delete(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub mod my_string_empty_as_none {
|
||||
use serde::{Deserializer, Serializer, Serialize};
|
||||
use std::str::FromStr;
|
||||
use std::fmt::{Display, Write};
|
||||
use std::marker::PhantomData;
|
||||
use serde::de::{Visitor, Error};
|
||||
use std::fmt;
|
||||
use yopa::ID;
|
||||
|
||||
// FIXME largely copied from serde_with
|
||||
|
||||
/// Deserialize an `Option<T>` from a string using `FromStr`
|
||||
pub fn deserialize<'de, D, S>(deserializer: D) -> Result<Option<S>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
S: FromStr,
|
||||
S::Err: Display,
|
||||
{
|
||||
struct OptionStringEmptyNone<S>(PhantomData<S>);
|
||||
impl<'de, S> Visitor<'de> for OptionStringEmptyNone<S>
|
||||
where
|
||||
S: FromStr,
|
||||
S::Err: Display,
|
||||
{
|
||||
type Value = Option<S>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("any string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
match value {
|
||||
"" => Ok(None),
|
||||
v => S::from_str(v).map(Some).map_err(Error::custom),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
match &*value {
|
||||
"" => Ok(None),
|
||||
v => S::from_str(v).map(Some).map_err(Error::custom),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO remove?
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
self.visit_str(&v.to_string())
|
||||
}
|
||||
|
||||
// handles the `null` case
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(OptionStringEmptyNone(PhantomData))
|
||||
}
|
||||
|
||||
/// Serialize a string from `Option<T>` using `AsRef<str>` or using the empty string if `None`.
|
||||
pub fn serialize<S>(option: &Option<ID>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
option.serialize(serializer)
|
||||
|
||||
// if let Some(value) = option {
|
||||
// value.serialize(serializer)
|
||||
// } else {
|
||||
// "".serialize(serializer)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,13 @@ pub(crate) async fn create_form(
|
||||
} else {
|
||||
context.insert(
|
||||
"old",
|
||||
// This is the defaults for the form
|
||||
&PropertyModelCreateForm {
|
||||
object: Default::default(),
|
||||
name: "".to_string(),
|
||||
optional: true,
|
||||
multiple: false,
|
||||
unique: false,
|
||||
data_type: DataType::String,
|
||||
default: "".to_string(),
|
||||
},
|
||||
@@ -70,6 +72,8 @@ pub(crate) struct PropertyModelCreateForm {
|
||||
pub optional: bool,
|
||||
#[serde(default)]
|
||||
pub multiple: bool,
|
||||
#[serde(default)]
|
||||
pub unique: bool,
|
||||
pub data_type: DataType,
|
||||
/// Default value to be parsed to the data type
|
||||
/// May be unused if empty and optional
|
||||
@@ -126,6 +130,7 @@ pub(crate) async fn create(
|
||||
|
||||
let optional = form.optional;
|
||||
let multiple = form.multiple;
|
||||
let unique = form.unique;
|
||||
|
||||
let default = match parse_default(form.data_type, form.default.clone()) {
|
||||
Ok(def) => def,
|
||||
@@ -143,6 +148,7 @@ pub(crate) async fn create(
|
||||
name: form.name.clone(),
|
||||
optional,
|
||||
multiple,
|
||||
unique,
|
||||
data_type: form.data_type,
|
||||
default,
|
||||
}) {
|
||||
@@ -190,6 +196,8 @@ pub(crate) struct PropertyModelEditForm {
|
||||
pub optional: bool,
|
||||
#[serde(default)]
|
||||
pub multiple: bool,
|
||||
#[serde(default)]
|
||||
pub unique: bool,
|
||||
pub data_type: DataType,
|
||||
/// Default value to be parsed to the data type
|
||||
/// May be unused if empty and optional
|
||||
@@ -253,6 +261,7 @@ pub(crate) async fn update(
|
||||
name: form.name.clone(),
|
||||
optional: form.optional,
|
||||
multiple: form.multiple,
|
||||
unique: form.unique,
|
||||
data_type: form.data_type,
|
||||
default,
|
||||
}) {
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
use crate::session_ext::SessionExt;
|
||||
use std::borrow::{Borrow, Cow};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_session::Session;
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
|
||||
use crate::tera_ext::TeraExt;
|
||||
use crate::utils::{redirect, StorageErrorIntoResponseError};
|
||||
use crate::TERA;
|
||||
use serde::Serialize;
|
||||
use yopa::data::Object;
|
||||
use yopa::insert::InsertObj;
|
||||
use yopa::{data, model, Storage, ID};
|
||||
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
use heck::TitleCase;
|
||||
use itertools::Itertools;
|
||||
use json_dotpath::DotPaths;
|
||||
use std::collections::HashMap;
|
||||
use serde::Serialize;
|
||||
|
||||
use yopa::{data, ID, model, Storage};
|
||||
use yopa::data::Object;
|
||||
use yopa::insert::InsertObj;
|
||||
use yopa::model::{ObjectModel, PropertyModel, RelationModel};
|
||||
use yopa::update::UpdateObj;
|
||||
|
||||
use crate::session_ext::SessionExt;
|
||||
use crate::TERA;
|
||||
use crate::tera_ext::TeraExt;
|
||||
use crate::utils::{redirect, StorageErrorIntoResponseError};
|
||||
|
||||
// we only need references here, Context serializes everything to Value.
|
||||
// cloning would be a waste of cycles
|
||||
|
||||
@@ -27,11 +29,18 @@ pub struct Schema<'a> {
|
||||
pub prop_models: Vec<&'a model::PropertyModel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ObjectDisplay<'a> {
|
||||
pub id: ID,
|
||||
pub model: ID,
|
||||
pub name: Cow<'a, str>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct ObjectCreateData<'a> {
|
||||
pub model_id: ID,
|
||||
pub schema: Schema<'a>,
|
||||
pub objects: Vec<&'a Object>,
|
||||
pub objects: Vec<ObjectDisplay<'a>>,
|
||||
}
|
||||
|
||||
#[get("/object/create/{model_id}")]
|
||||
@@ -76,16 +85,25 @@ fn prepare_object_create_data(rg: &Storage, model_id: ID) -> actix_web::Result<O
|
||||
related_ids.sort();
|
||||
related_ids.dedup();
|
||||
|
||||
let mut models_to_fetch = vec![model_id];
|
||||
models_to_fetch.extend(&related_ids);
|
||||
|
||||
Ok(ObjectCreateData {
|
||||
model_id: model.id,
|
||||
schema: Schema {
|
||||
obj_models: rg.get_object_models().collect(), // TODO get only the ones that matter here
|
||||
obj_models: rg.get_object_models_by_ids(models_to_fetch).collect(), // TODO get only the ones that matter here
|
||||
rel_models: relations,
|
||||
prop_models: rg
|
||||
.get_property_models_for_parents(prop_object_ids)
|
||||
.collect(),
|
||||
},
|
||||
objects: rg.get_objects_of_types(related_ids).collect(),
|
||||
objects: rg.get_objects_of_types(related_ids).map(|o| {
|
||||
ObjectDisplay {
|
||||
id: o.id,
|
||||
model: o.model,
|
||||
name: rg.get_object_name(o),
|
||||
}
|
||||
}).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -97,22 +115,17 @@ pub(crate) async fn create(
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
warn!("{:?}", form);
|
||||
|
||||
// let des : InsertObj = serde_json::from_value(form.into_inner()).unwrap();
|
||||
//
|
||||
// Ok(HttpResponse::Ok().finish())
|
||||
//
|
||||
|
||||
let mut wg = store.write().await;
|
||||
let form = form.into_inner();
|
||||
let name = form.name.clone();
|
||||
|
||||
let model_name = wg.get_model_name(form.model).to_owned().to_title_case();
|
||||
|
||||
match wg.insert_object(form) {
|
||||
Ok(_id) => {
|
||||
Ok(id) => {
|
||||
wg.persist().err_to_500()?;
|
||||
let obj_name = wg.get_object_name_by_id(id);
|
||||
debug!("Object created, redirecting to root");
|
||||
session.flash_success(format!("{} \"{}\" created.", model_name, name));
|
||||
session.flash_success(format!("{} \"{}\" created.", model_name, obj_name));
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -125,7 +138,7 @@ pub(crate) async fn create(
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
struct ModelWithObjects<'a> {
|
||||
model: &'a ObjectModel,
|
||||
objects: Vec<&'a Object>,
|
||||
objects: Vec<ObjectDisplay<'a>>,
|
||||
}
|
||||
|
||||
#[get("/objects")]
|
||||
@@ -148,8 +161,16 @@ pub(crate) async fn list_inner(
|
||||
.get_object_models()
|
||||
.sorted_by_key(|m| &m.name)
|
||||
.map(|model| {
|
||||
let mut objects = objects_by_model.remove(&model.id).unwrap_or_default();
|
||||
objects.sort_by_key(|o| &o.name);
|
||||
let objects = objects_by_model.remove(&model.id).unwrap_or_default();
|
||||
let mut objects = objects.into_iter().map(|o| {
|
||||
ObjectDisplay {
|
||||
id: o.id,
|
||||
model: o.model,
|
||||
name: rg.get_object_name(o), // TODO optimize
|
||||
}
|
||||
}).collect_vec();
|
||||
|
||||
objects.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
ModelWithObjects { model, objects }
|
||||
})
|
||||
.collect();
|
||||
@@ -176,7 +197,8 @@ struct RelationView<'a> {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RelationInstanceView<'a> {
|
||||
related: &'a Object,
|
||||
related: ObjectDisplay<'a>,
|
||||
related_name: Cow<'a, str>,
|
||||
properties: Vec<PropertyView<'a>>,
|
||||
}
|
||||
|
||||
@@ -201,7 +223,11 @@ pub(crate) async fn detail(
|
||||
.get_object_model(object.model)
|
||||
.ok_or_else(|| actix_web::error::ErrorNotFound("No such model"))?;
|
||||
|
||||
context.insert("object", object);
|
||||
context.insert("object", &ObjectDisplay {
|
||||
id: object_id,
|
||||
model: object.model,
|
||||
name: rg.get_object_name(object),
|
||||
});
|
||||
context.insert("model", model);
|
||||
context.insert("kind", &rg.get_model_name(object.model));
|
||||
|
||||
@@ -270,13 +296,20 @@ pub(crate) async fn detail(
|
||||
|
||||
view_rel_properties.sort_by_key(|p| &p.model.name);
|
||||
|
||||
let related_name = rg.get_object_name(related_obj);
|
||||
|
||||
instances.push(RelationInstanceView {
|
||||
related: related_obj,
|
||||
related: ObjectDisplay {
|
||||
id: related_obj.id,
|
||||
model: related_obj.model,
|
||||
name: rg.get_object_name(related_obj),
|
||||
},
|
||||
related_name,
|
||||
properties: view_rel_properties,
|
||||
})
|
||||
}
|
||||
|
||||
instances.sort_by_key(|r| &r.related.name);
|
||||
instances.sort_by(|a, b| a.related_name.cmp(&b.related_name));
|
||||
|
||||
relation_views.push(RelationView {
|
||||
model: rg.get_relation_model(model_id).unwrap(),
|
||||
@@ -322,13 +355,21 @@ pub(crate) async fn detail(
|
||||
|
||||
view_rel_properties.sort_by_key(|p| &p.model.name);
|
||||
|
||||
let related_name = rg.get_object_name(related_obj);
|
||||
|
||||
instances.push(RelationInstanceView {
|
||||
related: related_obj,
|
||||
related: ObjectDisplay {
|
||||
id: related_obj.id,
|
||||
model: related_obj.model,
|
||||
name: rg.get_object_name(related_obj),
|
||||
},
|
||||
related_name,
|
||||
properties: view_rel_properties,
|
||||
})
|
||||
}
|
||||
|
||||
instances.sort_by_key(|r| &r.related.name);
|
||||
// sort_by_key is not possible because rust is still stupid about lifetimes
|
||||
instances.sort_by(|a, b| a.related_name.cmp(&b.related_name));
|
||||
|
||||
relation_views.push(RelationView {
|
||||
model: rg.get_relation_model(model_id).unwrap(),
|
||||
@@ -349,7 +390,7 @@ pub(crate) async fn detail(
|
||||
struct EnrichedObject<'a> {
|
||||
id: ID,
|
||||
model: ID,
|
||||
name: String,
|
||||
name: Cow<'a, str>,
|
||||
values: HashMap<
|
||||
String, /* ID but as string so serde will stop exploding */
|
||||
Vec<&'a data::Value>,
|
||||
@@ -387,7 +428,11 @@ pub(crate) async fn update_form(
|
||||
|
||||
// maybe its useful,idk
|
||||
context.insert("model", &model);
|
||||
context.insert("object", &object);
|
||||
context.insert("object", &ObjectDisplay {
|
||||
id: object.id,
|
||||
model: object.model,
|
||||
name: rg.get_object_name(object),
|
||||
});
|
||||
|
||||
let create_data = prepare_object_create_data(&rg, model.id)?;
|
||||
|
||||
@@ -480,7 +525,7 @@ pub(crate) async fn update_form(
|
||||
let object = EnrichedObject {
|
||||
id: object.id,
|
||||
model: object.model,
|
||||
name: object.name.clone(),
|
||||
name: rg.get_object_name_by_id(object.id),
|
||||
values: value_map,
|
||||
relations: relation_map,
|
||||
};
|
||||
@@ -494,26 +539,26 @@ pub(crate) async fn update_form(
|
||||
|
||||
#[post("/object/update/{id}")]
|
||||
pub(crate) async fn update(
|
||||
_id: web::Path<ID>,
|
||||
id: web::Path<ID>,
|
||||
form: web::Json<UpdateObj>,
|
||||
store: crate::YopaStoreWrapper,
|
||||
session: Session,
|
||||
) -> actix_web::Result<HttpResponse> {
|
||||
let mut wg = store.write().await;
|
||||
let form = form.into_inner();
|
||||
let name = form.name.clone();
|
||||
|
||||
let object = wg
|
||||
.get_object(form.id)
|
||||
.get_object(*id)
|
||||
.ok_or_else(|| actix_web::error::ErrorNotFound("No such object"))?;
|
||||
|
||||
let model_name = wg.get_model_name(object.model).to_owned().to_title_case();
|
||||
let id = object.id;
|
||||
|
||||
match wg.update_object(form) {
|
||||
Ok(_id) => {
|
||||
wg.persist().err_to_500()?;
|
||||
debug!("Object created, redirecting to root");
|
||||
session.flash_success(format!("{} \"{}\" updated.", model_name, name));
|
||||
session.flash_success(format!("{} \"{}\" updated.", model_name, wg.get_object_name_by_id(id)));
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -530,11 +575,14 @@ pub(crate) async fn delete(
|
||||
session: Session,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let mut wg = store.write().await;
|
||||
|
||||
let name = wg.get_object_name_by_id(*id).to_string();
|
||||
|
||||
match wg.delete_object(*id) {
|
||||
Ok(obj) => {
|
||||
Ok(_obj) => {
|
||||
wg.persist().err_to_500()?;
|
||||
debug!("Object deleted, redirecting to root");
|
||||
session.flash_success(format!("Object \"{}\" deleted.", obj.name));
|
||||
session.flash_success(format!("Object \"{}\" deleted.", name));
|
||||
redirect("/")
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -39,3 +39,19 @@ impl<K, V> KVVecToKeysOrValues<K, V> for Vec<(K, V)> {
|
||||
self.into_iter().map(|(_k, v)| v).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait IsNoneOrElse<T> : Sized {
|
||||
//noinspection RsSelfConvention
|
||||
fn is_none_or_else(&self, test : impl FnOnce(&T) -> bool) -> bool;
|
||||
}
|
||||
|
||||
impl<T> IsNoneOrElse<T> for Option<T> {
|
||||
fn is_none_or_else(&self, test: impl FnOnce(&T) -> bool) -> bool {
|
||||
match self {
|
||||
None => true,
|
||||
Some(value) => {
|
||||
test(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+67
-59
@@ -1,6 +1,6 @@
|
||||
//! Data value structs
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::borrow::{Cow, Borrow};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -35,6 +35,16 @@ impl Display for TypedValue {
|
||||
}
|
||||
|
||||
impl TypedValue {
|
||||
/// To Cow<str>
|
||||
pub fn to_cow(&self) -> Cow<str> {
|
||||
match self {
|
||||
TypedValue::String(v) => Cow::Borrowed(v.borrow()),
|
||||
TypedValue::Integer(v) => v.to_string().into(),
|
||||
TypedValue::Decimal(v) => v.to_string().into(),
|
||||
TypedValue::Boolean(v) => if *v { "True" } else { "False" }.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
match (self, ty) {
|
||||
@@ -83,6 +93,62 @@ impl TypedValue {
|
||||
}
|
||||
}
|
||||
|
||||
/// Instance of an object
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Object {
|
||||
/// PK
|
||||
#[serde(default)]
|
||||
pub id: ID,
|
||||
/// Object template ID
|
||||
pub model: ID,
|
||||
}
|
||||
|
||||
/// Relation between two objects
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Relation {
|
||||
/// PK
|
||||
#[serde(default)]
|
||||
pub id: ID,
|
||||
/// Source object ID
|
||||
pub object: ID,
|
||||
/// Relation template ID
|
||||
pub model: ID,
|
||||
/// Related object ID
|
||||
pub related: ID,
|
||||
}
|
||||
|
||||
/// Value attached to an object
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Value {
|
||||
/// PK
|
||||
#[serde(default)]
|
||||
pub id: ID,
|
||||
/// Owning object ID
|
||||
pub object: ID,
|
||||
/// Property template ID
|
||||
pub model: ID,
|
||||
/// Property value
|
||||
pub value: TypedValue,
|
||||
}
|
||||
|
||||
impl HaveId for Object {
|
||||
fn get_id(&self) -> ID {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl HaveId for Relation {
|
||||
fn get_id(&self) -> ID {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl HaveId for Value {
|
||||
fn get_id(&self) -> ID {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::data::TypedValue;
|
||||
@@ -300,61 +366,3 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Instance of an object
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Object {
|
||||
/// PK
|
||||
#[serde(default)]
|
||||
pub id: ID,
|
||||
/// Object template ID
|
||||
pub model: ID,
|
||||
/// Model name, mainly shown in lists
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Relation between two objects
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Relation {
|
||||
/// PK
|
||||
#[serde(default)]
|
||||
pub id: ID,
|
||||
/// Source object ID
|
||||
pub object: ID,
|
||||
/// Relation template ID
|
||||
pub model: ID,
|
||||
/// Related object ID
|
||||
pub related: ID,
|
||||
}
|
||||
|
||||
/// Value attached to an object
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Value {
|
||||
/// PK
|
||||
#[serde(default)]
|
||||
pub id: ID,
|
||||
/// Owning object ID
|
||||
pub object: ID,
|
||||
/// Property template ID
|
||||
pub model: ID,
|
||||
/// Property value
|
||||
pub value: TypedValue,
|
||||
}
|
||||
|
||||
impl HaveId for Object {
|
||||
fn get_id(&self) -> ID {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl HaveId for Relation {
|
||||
fn get_id(&self) -> ID {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl HaveId for Value {
|
||||
fn get_id(&self) -> ID {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ impl InsertRel {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InsertObj {
|
||||
pub model: ID,
|
||||
pub name: String,
|
||||
pub values: Vec<InsertValue>,
|
||||
pub relations: Vec<InsertRel>,
|
||||
}
|
||||
@@ -51,13 +50,11 @@ pub struct InsertObj {
|
||||
impl InsertObj {
|
||||
pub fn new(
|
||||
model_id: ID,
|
||||
name: String,
|
||||
values: Vec<InsertValue>,
|
||||
relations: Vec<InsertRel>,
|
||||
) -> Self {
|
||||
Self {
|
||||
model: model_id,
|
||||
name,
|
||||
values,
|
||||
relations,
|
||||
}
|
||||
|
||||
+142
-48
@@ -6,7 +6,7 @@ extern crate log;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{BufReader, BufWriter};
|
||||
use std::io::{BufReader, BufWriter, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use itertools::Itertools;
|
||||
@@ -23,6 +23,8 @@ use model::ObjectModel;
|
||||
|
||||
use crate::model::{PropertyModel, RelationModel};
|
||||
use crate::update::{UpdateObj, UpsertValue};
|
||||
use crate::cool::IsNoneOrElse;
|
||||
use crate::data::{Object, Value};
|
||||
|
||||
mod cool;
|
||||
pub mod data;
|
||||
@@ -38,6 +40,9 @@ mod tests;
|
||||
|
||||
pub const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub const YOPA_MAGIC : &[u8; 4] = b"YOPA";
|
||||
pub const BINARY_FORMAT : u8 = 1;
|
||||
|
||||
/// Stupid storage with naive inefficient file persistence
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Storage {
|
||||
@@ -90,8 +95,14 @@ pub enum StorageError {
|
||||
NotExist(Cow<'static, str>),
|
||||
#[error("{0}")]
|
||||
ConstraintViolation(Cow<'static, str>),
|
||||
#[error("{0}")]
|
||||
Invalid(Cow<'static, str>),
|
||||
#[error("Persistence not configured!")]
|
||||
NotPersistent,
|
||||
#[error("Bad magic! Not a binary Yopa file")]
|
||||
BadMagic,
|
||||
#[error("Binary format {0} is not compatible with this version of Yopa")]
|
||||
NotCompatible(u8),
|
||||
#[error(transparent)]
|
||||
IO(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
@@ -164,11 +175,25 @@ impl Storage {
|
||||
|
||||
let f = OpenOptions::new().read(true).open(&path)?;
|
||||
|
||||
let reader = BufReader::new(f);
|
||||
let mut reader = BufReader::new(f);
|
||||
|
||||
let parsed: Self = match self.opts.file_format {
|
||||
FileEncoding::JSON => serde_json::from_reader(reader)?,
|
||||
FileEncoding::BINCODE => bincode::deserialize_from(reader)?,
|
||||
FileEncoding::BINCODE => {
|
||||
let mut magic : [u8; 5] = [0; 5];
|
||||
reader.read_exact(&mut magic)?;
|
||||
|
||||
if &magic[0..4] != YOPA_MAGIC {
|
||||
return Err(StorageError::BadMagic);
|
||||
}
|
||||
|
||||
let version = magic[4];
|
||||
if version != BINARY_FORMAT {
|
||||
return Err(StorageError::NotCompatible(version));
|
||||
}
|
||||
|
||||
bincode::deserialize_from(reader)?
|
||||
},
|
||||
};
|
||||
|
||||
let opts = std::mem::replace(&mut self.opts, StoreOpts::default());
|
||||
@@ -196,13 +221,17 @@ impl Storage {
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&path)?;
|
||||
let writer = BufWriter::new(f);
|
||||
let mut writer = BufWriter::new(f);
|
||||
|
||||
match self.opts.file_format {
|
||||
FileEncoding::JSON => {
|
||||
serde_json::to_writer(writer, self)?;
|
||||
}
|
||||
FileEncoding::BINCODE => bincode::serialize_into(writer, self)?,
|
||||
FileEncoding::BINCODE => {
|
||||
writer.write_all(YOPA_MAGIC)?;
|
||||
writer.write_all(&[BINARY_FORMAT])?;
|
||||
bincode::serialize_into(writer, self)?
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -223,6 +252,25 @@ impl Storage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get object name, or the ID as string if no name is configured
|
||||
pub fn get_object_name_by_id<'a>(&'a self, id : ID) -> Cow<'a, str> {
|
||||
if let Some(o) = self.get_object(id) {
|
||||
return self.get_object_name(o);
|
||||
}
|
||||
|
||||
return id.to_string().into();
|
||||
}
|
||||
|
||||
pub fn get_object_name<'b, 'a: 'b>(&'a self, object : &'a Object) -> Cow<'b, str> {
|
||||
if let Some(ObjectModel{ name_property: Some(name_property), .. }) = self.get_object_model(object.model) {
|
||||
if let Some(v) = self.values.values().find(|v| v.object == object.id && &v.model == name_property) {
|
||||
return v.value.to_cow();
|
||||
}
|
||||
}
|
||||
|
||||
return object.id.to_string().into();
|
||||
}
|
||||
|
||||
/// Get model name. Accepts ID of object, relation or property models.
|
||||
pub fn get_model_name(&self, id: ID) -> &str {
|
||||
if let Some(x) = self.obj_models.get(&id) {
|
||||
@@ -243,6 +291,11 @@ impl Storage {
|
||||
self.obj_models.values()
|
||||
}
|
||||
|
||||
/// Iterate object models with given IDs
|
||||
pub fn get_object_models_by_ids(&self, ids: Vec<ID>) -> impl Iterator<Item = &ObjectModel> {
|
||||
self.obj_models.values().filter(move |m| ids.contains(&m.id))
|
||||
}
|
||||
|
||||
/// Get an object model by ID
|
||||
pub fn get_object_model(&self, model_id: ID) -> Option<&ObjectModel> {
|
||||
self.obj_models.get(&model_id)
|
||||
@@ -297,6 +350,16 @@ impl Storage {
|
||||
.filter(move |model| parents.contains(&model.object))
|
||||
}
|
||||
|
||||
/// Find properties for a parent ID
|
||||
pub fn get_property_models_for_parent(
|
||||
&self,
|
||||
parent: ID,
|
||||
) -> impl Iterator<Item = &PropertyModel> {
|
||||
self.prop_models
|
||||
.values()
|
||||
.filter(move |model| model.object == parent)
|
||||
}
|
||||
|
||||
/// Get all relation models, grouped by their source object model ID
|
||||
pub fn get_grouped_relation_models(&self) -> HashMap<ID, Vec<&RelationModel>> {
|
||||
self.rel_models
|
||||
@@ -485,6 +548,12 @@ impl Storage {
|
||||
}
|
||||
}
|
||||
|
||||
if prop.unique && prop.multiple {
|
||||
return Err(StorageError::Invalid(
|
||||
"Multi-value properties cannot have the \"unique\" constraint".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if self
|
||||
.prop_models
|
||||
.iter()
|
||||
@@ -688,6 +757,12 @@ impl Storage {
|
||||
));
|
||||
}
|
||||
|
||||
if prop.unique && prop.multiple {
|
||||
return Err(StorageError::Invalid(
|
||||
"Multi-value properties cannot have the \"unique\" constraint".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Object can't be changed, so we re-fill them from the existing model
|
||||
if let Some(existing) = self.prop_models.get(&prop.id) {
|
||||
prop.object = existing.object;
|
||||
@@ -745,28 +820,10 @@ impl Storage {
|
||||
}
|
||||
};
|
||||
|
||||
// validate unique name
|
||||
if self
|
||||
.objects
|
||||
.iter()
|
||||
.find(|(_, o)| o.model == obj_model_id && o.name == insobj.name)
|
||||
.is_some()
|
||||
{
|
||||
return Err(StorageError::ConstraintViolation(
|
||||
format!(
|
||||
"{} named \"{}\" already exists",
|
||||
self.get_model_name(obj_model_id),
|
||||
insobj.name
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
let object_id = self.next_id();
|
||||
let object = data::Object {
|
||||
id: object_id,
|
||||
model: obj_model_id,
|
||||
name: insobj.name,
|
||||
};
|
||||
|
||||
let find_values_to_insert = |values: Vec<InsertValue>,
|
||||
@@ -776,12 +833,12 @@ impl Storage {
|
||||
let mut values_by_id = values.into_iter().into_group_map_by(|iv| iv.model);
|
||||
let mut values_to_insert = vec![];
|
||||
|
||||
for (id, prop) in self
|
||||
for (prop_model_id, prop) in self
|
||||
.prop_models
|
||||
.iter()
|
||||
.filter(|(_id, p)| p.object == parent_model_id)
|
||||
{
|
||||
if let Some(values) = values_by_id.remove(id) {
|
||||
if let Some(values) = values_by_id.remove(prop_model_id) {
|
||||
if values.len() > 1 && !prop.multiple {
|
||||
return Err(StorageError::ConstraintViolation(
|
||||
format!(
|
||||
@@ -792,6 +849,35 @@ impl Storage {
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
if prop.unique {
|
||||
// we know the length is at least 1. Unique should not be allowed together
|
||||
// with "multiple", but if it is set so, only the first value will
|
||||
// be checked.
|
||||
let first = &values[0];
|
||||
|
||||
// validate unique name
|
||||
if self
|
||||
.values
|
||||
.iter()
|
||||
.find(|(_, o)| {
|
||||
&o.model == prop_model_id
|
||||
&& o.object == parent_id
|
||||
&& o.value == first.value
|
||||
})
|
||||
.is_some()
|
||||
{
|
||||
return Err(StorageError::ConstraintViolation(
|
||||
format!(
|
||||
"Value {} already used for unique property \"{}\"",
|
||||
first.value,
|
||||
self.get_model_name(*prop_model_id),
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for val_instance in values {
|
||||
values_to_insert.push(data::Value {
|
||||
id: self.next_id(),
|
||||
@@ -920,27 +1006,6 @@ impl Storage {
|
||||
}
|
||||
};
|
||||
|
||||
// validate unique name
|
||||
if self
|
||||
.objects
|
||||
.iter()
|
||||
.find(|(_, o)| {
|
||||
o.model == updated_object_model_id
|
||||
&& o.name == updobj.name
|
||||
&& o.id != updated_object_id
|
||||
})
|
||||
.is_some()
|
||||
{
|
||||
return Err(StorageError::ConstraintViolation(
|
||||
format!(
|
||||
"{} named \"{}\" already exists",
|
||||
self.get_model_name(updated_object_model_id),
|
||||
updobj.name
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Update the object after everything else is checked
|
||||
|
||||
let find_values_to_change = |values: Vec<UpsertValue>,
|
||||
@@ -982,6 +1047,35 @@ impl Storage {
|
||||
));
|
||||
}
|
||||
|
||||
if prop.unique {
|
||||
// we know the length is at least 1. Unique should not be allowed together
|
||||
// with "multiple", but if it is set so, only the first value will
|
||||
// be checked.
|
||||
let first = &values[0];
|
||||
|
||||
// validate unique name
|
||||
if self
|
||||
.values
|
||||
.iter()
|
||||
.find(|(_, o)| {
|
||||
&o.model == prop_model_id
|
||||
&& o.object == parent_id
|
||||
&& o.value == first.value
|
||||
&& (first.id.is_none_or_else(|id| &o.id != id))
|
||||
})
|
||||
.is_some()
|
||||
{
|
||||
return Err(StorageError::ConstraintViolation(
|
||||
format!(
|
||||
"Value {} already used for unique property \"{}\"",
|
||||
first.value,
|
||||
self.get_model_name(*prop_model_id),
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let updated_ids = values.iter().filter_map(|v| v.id).collect_vec();
|
||||
|
||||
ids_to_delete.extend(
|
||||
@@ -1103,7 +1197,6 @@ impl Storage {
|
||||
}
|
||||
|
||||
let obj_mut = self.objects.get_mut(&updated_object_id).unwrap();
|
||||
obj_mut.name = updobj.name;
|
||||
|
||||
debug!("Add {} new object relations", relations_to_insert.len());
|
||||
for rel in relations_to_insert {
|
||||
@@ -1130,8 +1223,9 @@ impl Storage {
|
||||
|
||||
/// Delete an object and associated data
|
||||
pub fn delete_object(&mut self, id: ID) -> Result<data::Object, StorageError> {
|
||||
let name = self.get_object_name_by_id(id).to_string();
|
||||
return if let Some(t) = self.objects.remove(&id) {
|
||||
debug!("Delete object \"{}\"", t.name);
|
||||
debug!("Delete object \"{}\"", name);
|
||||
// Remove relation templates
|
||||
let removed_relation_ids = map_drain_filter(&mut self.relations, |_k, v| {
|
||||
v.object == id || v.related == id
|
||||
|
||||
@@ -23,6 +23,9 @@ pub struct ObjectModel {
|
||||
pub id: ID,
|
||||
/// Template name, unique within the database
|
||||
pub name: String,
|
||||
/// Property to use as the name in relation selectors
|
||||
#[serde(default)]
|
||||
pub name_property: Option<ID>,
|
||||
}
|
||||
|
||||
/// Relation between templates
|
||||
@@ -59,6 +62,8 @@ pub struct PropertyModel {
|
||||
pub optional: bool,
|
||||
/// Property can be multiple
|
||||
pub multiple: bool,
|
||||
/// Value must be unique among instances of the parent model
|
||||
pub unique: bool,
|
||||
/// Property data type
|
||||
pub data_type: DataType,
|
||||
/// Default value, used for newly created objects
|
||||
|
||||
+6
-2
@@ -22,16 +22,20 @@ pub struct UpsertRelation {
|
||||
pub model: ID,
|
||||
/// Related model ID
|
||||
pub related: ID,
|
||||
/// Relation values
|
||||
/// Relation values to update or create
|
||||
pub values: Vec<UpsertValue>,
|
||||
}
|
||||
|
||||
/// Update an existing object
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct UpdateObj {
|
||||
/// Updated object's ID
|
||||
pub id: ID,
|
||||
pub name: String,
|
||||
/// Property to use as the object's name
|
||||
pub name_property: Option<ID>,
|
||||
/// Values to update or create
|
||||
pub values: Vec<UpsertValue>,
|
||||
/// Relations to update or create
|
||||
pub relations: Vec<UpsertRelation>,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user