names are now a property and can be chosen arbitrarily

master
Ondřej Hruška 3 years ago
parent fbd2a1ed75
commit e810724cd1
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 72
      Cargo.lock
  2. 1
      yopa-web/Cargo.toml
  3. 8
      yopa-web/resources/src/components/EditObjectForm.vue
  4. 13
      yopa-web/resources/src/components/NewObjectForm.vue
  5. 2
      yopa-web/resources/static/bundle.js
  6. 2
      yopa-web/resources/static/bundle.js.map
  7. 9
      yopa-web/resources/templates/models/model_create.html.tera
  8. 20
      yopa-web/resources/templates/models/model_update.html.tera
  9. 67
      yopa-web/resources/templates/models/property_create.html.tera
  10. 74
      yopa-web/resources/templates/models/property_update.html.tera
  11. 55
      yopa-web/resources/templates/models/relation_create.html.tera
  12. 35
      yopa-web/resources/templates/models/relation_update.html.tera
  13. 103
      yopa-web/src/routes/models/object.rs
  14. 9
      yopa-web/src/routes/models/property.rs
  15. 130
      yopa-web/src/routes/objects.rs
  16. 16
      yopa/src/cool.rs
  17. 126
      yopa/src/data.rs
  18. 3
      yopa/src/insert.rs
  19. 190
      yopa/src/lib.rs
  20. 5
      yopa/src/model.rs
  21. 8
      yopa/src/update.rs

72
Cargo.lock generated

@ -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>
<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>
<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>
</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>
<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>
<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>
<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">
</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>
</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>
<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>
<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>
<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>
<table>
<tr>
<th><label for="name">Name:</label></th>
<td>
<input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"><br>
</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">
</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>
</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 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 std::borrow::{Borrow, Cow};
use std::collections::HashMap;
use actix_session::Session;
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)
}
}
}
}

@ -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,
}

@ -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

@ -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>,
}

Loading…
Cancel
Save