commit
6c54873ca2
34 changed files with 5502 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/target
|
||||||
|
.env
|
||||||
|
/db
|
||||||
|
/files
|
||||||
|
/owl_macro/target
|
9
.woodpecker/test.yml
Normal file
9
.woodpecker/test.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: main
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: "Cargo Test"
|
||||||
|
image: rust:alpine
|
||||||
|
commands:
|
||||||
|
- cargo test --all
|
2730
Cargo.lock
generated
Normal file
2730
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
27
Cargo.toml
Normal file
27
Cargo.toml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
name = "owl"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
|
log = "0.4.20"
|
||||||
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
|
serde_json = "1.0.111"
|
||||||
|
tokio = { version = "1.35.1", features = ["full"] }
|
||||||
|
uuid = { version = "1.8.0", features = ["v4", "serde"] }
|
||||||
|
sha2 = "0.10.8"
|
||||||
|
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-native-tls", "derive", "uuid", "chrono", "json"] }
|
||||||
|
geozero = "0.14.0"
|
||||||
|
ulid = { version = "1.2.1", features = ["serde"] }
|
||||||
|
owl_macro = { path = "./owl_macro" }
|
||||||
|
vfs = "0.12.1"
|
||||||
|
dashmap = "6.1.0"
|
||||||
|
rayon = "1.10.0"
|
||||||
|
argh = "0.1.13"
|
||||||
|
rmp-serde = "1.3.0"
|
||||||
|
rmpv = { version = "1.3.0", features = ["serde", "with-serde"] }
|
||||||
|
env_logger = "0.11.8"
|
||||||
|
parking_lot = { version = "0.12.3", features = ["send_guard"] }
|
||||||
|
crossbeam = { version = "0.8.4", features = ["crossbeam-channel"] }
|
||||||
|
once_cell = "1.21.3"
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
FROM rust:buster as builder
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
FROM debian:buster
|
||||||
|
|
||||||
|
RUN apt update && apt upgrade -y
|
||||||
|
RUN apt install -y ca-certificates openssl
|
||||||
|
|
||||||
|
COPY --from=builder /app/target/release/owl /owl
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
CMD ["/owl"]
|
39
README.md
Normal file
39
README.md
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# 🦉 owl
|
||||||
|
owl provides a model based database with references and relations.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
Simple embedded database:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use owl::prelude::*;
|
||||||
|
|
||||||
|
#[model]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Item {
|
||||||
|
pub id: Id,
|
||||||
|
pub cost: f64,
|
||||||
|
pub strength: f64
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
// Init
|
||||||
|
let db = Database::in_memory();
|
||||||
|
|
||||||
|
// Save
|
||||||
|
let item = Item {
|
||||||
|
id: Id::new_ulid(),
|
||||||
|
cost: 1.20,
|
||||||
|
strength: 0.4,
|
||||||
|
};
|
||||||
|
|
||||||
|
dbg!(&item);
|
||||||
|
db.save(&item);
|
||||||
|
|
||||||
|
// Get
|
||||||
|
let i: Item = db.get(&item.id.to_string());
|
||||||
|
|
||||||
|
dbg!(i);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For more usage examples look at the `./examples` directory.
|
81
examples/basic.rs
Normal file
81
examples/basic.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use owl::prelude::*;
|
||||||
|
|
||||||
|
#[model]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Item {
|
||||||
|
pub id: Id,
|
||||||
|
pub cost: f64,
|
||||||
|
pub strength: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
// Init
|
||||||
|
let db = Database::in_memory();
|
||||||
|
|
||||||
|
// Save
|
||||||
|
let item = Item {
|
||||||
|
id: Id::new_ulid(),
|
||||||
|
cost: 1.80,
|
||||||
|
strength: 0.4,
|
||||||
|
};
|
||||||
|
|
||||||
|
let first_id = item.id.clone();
|
||||||
|
|
||||||
|
dbg!(&item);
|
||||||
|
let item = db.save(item);
|
||||||
|
|
||||||
|
// Get
|
||||||
|
let i: Model<Item> = db.get(item.read().id.clone()).unwrap();
|
||||||
|
|
||||||
|
dbg!(&i.read());
|
||||||
|
|
||||||
|
db.save(Item {
|
||||||
|
id: Id::new_ulid(),
|
||||||
|
cost: 0.3,
|
||||||
|
strength: 2.4,
|
||||||
|
});
|
||||||
|
|
||||||
|
db.save(Item {
|
||||||
|
id: Id::new_ulid(),
|
||||||
|
cost: 3.4,
|
||||||
|
strength: 0.4,
|
||||||
|
});
|
||||||
|
|
||||||
|
db.save(Item {
|
||||||
|
id: Id::new_ulid(),
|
||||||
|
cost: 20.0,
|
||||||
|
strength: 200.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
db.save(Item {
|
||||||
|
id: Id::new_ulid(),
|
||||||
|
cost: 4.2,
|
||||||
|
strength: 4.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query
|
||||||
|
let res = db.query(|x: &Item| x.cost > 1.5);
|
||||||
|
dbg!(&res
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| format!("{:?}", x.read()))
|
||||||
|
.collect::<Vec<_>>());
|
||||||
|
|
||||||
|
// Update
|
||||||
|
db.update(&mut db.query(|x: &Item| x.cost > 1.5), |x: &mut Item| {
|
||||||
|
x.cost += 1.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
let item: Model<Item> = db.get(first_id.to_string().as_str()).unwrap();
|
||||||
|
dbg!(&item.read());
|
||||||
|
assert_eq!(item.read().cost, 2.80);
|
||||||
|
|
||||||
|
// Aggregates
|
||||||
|
let count = db.query(|x: &Item| x.cost > 1.5).len();
|
||||||
|
let sum: f64 = db
|
||||||
|
.query(|x: &Item| x.cost > 1.5)
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.read().cost)
|
||||||
|
.sum();
|
||||||
|
dbg!(count);
|
||||||
|
dbg!(sum);
|
||||||
|
}
|
81
examples/basic_global.rs
Normal file
81
examples/basic_global.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use owl::{get, prelude::*, query, save, set_global_db, update};
|
||||||
|
|
||||||
|
#[model]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Item {
|
||||||
|
pub id: Id,
|
||||||
|
pub cost: f64,
|
||||||
|
pub strength: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
// Init
|
||||||
|
let db = Database::in_memory();
|
||||||
|
set_global_db!(db);
|
||||||
|
|
||||||
|
// Save
|
||||||
|
let item = Item {
|
||||||
|
id: Id::new_ulid(),
|
||||||
|
cost: 1.80,
|
||||||
|
strength: 0.4,
|
||||||
|
};
|
||||||
|
|
||||||
|
let first_id = item.id.clone();
|
||||||
|
|
||||||
|
dbg!(&item);
|
||||||
|
let item = save!(item);
|
||||||
|
|
||||||
|
// Get
|
||||||
|
let i: Model<Item> = get!(item.read().id.clone()).unwrap();
|
||||||
|
|
||||||
|
dbg!(&i.read());
|
||||||
|
|
||||||
|
save!(Item {
|
||||||
|
id: Id::new_ulid(),
|
||||||
|
cost: 0.3,
|
||||||
|
strength: 2.4,
|
||||||
|
});
|
||||||
|
|
||||||
|
save!(Item {
|
||||||
|
id: Id::new_ulid(),
|
||||||
|
cost: 3.4,
|
||||||
|
strength: 0.4,
|
||||||
|
});
|
||||||
|
|
||||||
|
save!(Item {
|
||||||
|
id: Id::new_ulid(),
|
||||||
|
cost: 20.0,
|
||||||
|
strength: 200.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
save!(Item {
|
||||||
|
id: Id::new_ulid(),
|
||||||
|
cost: 4.2,
|
||||||
|
strength: 4.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query
|
||||||
|
let res = query!(|x: &Item| x.cost > 1.5);
|
||||||
|
dbg!(&res
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| format!("{:?}", x.read()))
|
||||||
|
.collect::<Vec<_>>());
|
||||||
|
|
||||||
|
// Update
|
||||||
|
update!(&mut query!(|x: &Item| x.cost > 1.5), |x: &mut Item| {
|
||||||
|
x.cost += 1.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
let item: Model<Item> = get!(first_id.to_string().as_str()).unwrap();
|
||||||
|
dbg!(&item.read());
|
||||||
|
assert_eq!(item.read().cost, 2.80);
|
||||||
|
|
||||||
|
// Aggregates
|
||||||
|
let count = query!(|x: &Item| x.cost > 1.5).len();
|
||||||
|
let sum: f64 = query!(|x: &Item| x.cost > 1.5)
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.read().cost)
|
||||||
|
.sum();
|
||||||
|
dbg!(count);
|
||||||
|
dbg!(sum);
|
||||||
|
}
|
17
examples/blob.rs
Normal file
17
examples/blob.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use owl::{db::model::file::File, prelude::*, Identifiable};
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
// Init
|
||||||
|
let db = Database::in_memory();
|
||||||
|
|
||||||
|
let f = File::new(
|
||||||
|
include_bytes!("../Cargo.toml").to_vec(),
|
||||||
|
Some("Cargo.toml".to_string()),
|
||||||
|
&db,
|
||||||
|
);
|
||||||
|
dbg!(&f);
|
||||||
|
|
||||||
|
let f: Model<File> = db.get(f.id()).unwrap();
|
||||||
|
let data = String::from_utf8(f.read_file(&db)).unwrap();
|
||||||
|
println!("Content: {data}");
|
||||||
|
}
|
80
examples/friends.rs
Normal file
80
examples/friends.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
use owl::{
|
||||||
|
db::{
|
||||||
|
model::person::Person,
|
||||||
|
relation::{clean_graph_traversal, find_path, get_other},
|
||||||
|
},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[relation("friend", Person, "friendy", Person, RelationKind::Unidirectional)]
|
||||||
|
pub struct Friendship;
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let db = Database::filesystem("./db");
|
||||||
|
let alice = Person::new_id("alice", "", "");
|
||||||
|
let alice = db.save(alice);
|
||||||
|
let bob = Person::new_id("bob", "", "");
|
||||||
|
let bob = db.save(bob);
|
||||||
|
|
||||||
|
Friendship {}.add(&alice, &bob, None, &db);
|
||||||
|
|
||||||
|
let charizard = Person::new_id("charizard", "", "");
|
||||||
|
let charizard = db.save(charizard);
|
||||||
|
|
||||||
|
Friendship.add(&alice, &charizard, None, &db);
|
||||||
|
Friendship.add(&charizard, &bob, None, &db);
|
||||||
|
|
||||||
|
let pika = db.save(Person::new_id("pika", "", ""));
|
||||||
|
|
||||||
|
Friendship.add(&pika, &charizard, None, &db);
|
||||||
|
|
||||||
|
let malice = db.save(Person::new_id("malice", "", ""));
|
||||||
|
|
||||||
|
Friendship.add(&pika, &malice, None, &db);
|
||||||
|
|
||||||
|
let drache = db.save(Person::new_id("drache", "", ""));
|
||||||
|
Friendship.add(&drache, &bob, None, &db);
|
||||||
|
Friendship.add(&drache, &malice, None, &db);
|
||||||
|
|
||||||
|
let enid = db.save(Person::new_id("enid", "", ""));
|
||||||
|
|
||||||
|
Friendship.add(&enid, &alice, None, &db);
|
||||||
|
|
||||||
|
print_friends("person::alice", &db);
|
||||||
|
print_friends("person::bob", &db);
|
||||||
|
print_friends("person::charizard", &db);
|
||||||
|
print_friends("person::drache", &db);
|
||||||
|
print_friends("person::enid", &db);
|
||||||
|
print_friends("person::malice", &db);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"alice to malice? - {:?}",
|
||||||
|
clean_graph_traversal(
|
||||||
|
"person::alice",
|
||||||
|
&find_path(
|
||||||
|
"person::alice".into(),
|
||||||
|
"person::malice".into(),
|
||||||
|
6,
|
||||||
|
|id, db| get_friends_of(id, &db),
|
||||||
|
&db
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_friends(id: &str, db: &Database) {
|
||||||
|
let other: Vec<_> = get_friends_of(id, db);
|
||||||
|
println!("friends of {id} -- {other:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_friends_of<T: Into<IdRef<Person>>>(id: T, db: &Database) -> Vec<IdRef<Person>> {
|
||||||
|
let id: IdRef<Person> = id.into();
|
||||||
|
let refs = Friendship::get_friend_of(id.to_string(), db);
|
||||||
|
dbg!(&refs);
|
||||||
|
get_other::<Person, _, _>(id, refs, db)
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| IdRef::<Person>::from(&x))
|
||||||
|
.collect()
|
||||||
|
}
|
55
examples/loan.rs
Normal file
55
examples/loan.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use owl::{db::model::person::Person, prelude::*};
|
||||||
|
#[relation("receiver", Person, "payer", Person, RelationKind::Bidirectional)]
|
||||||
|
pub struct LoanRelation {
|
||||||
|
debt: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lend_money(who: &Model<Person>, from: &Model<Person>, amount: f64, db: &Database) {
|
||||||
|
LoanRelation { debt: amount }.add(who, from, None, db)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pay_off(who: &Model<Person>, to: &Model<Person>, amount: f64, db: &Database) {
|
||||||
|
let mut loan = LoanRelation::get(who, to, db).unwrap();
|
||||||
|
loan.write(db, |loan| {
|
||||||
|
loan.alter_meta(|x: &mut _| {
|
||||||
|
x.debt -= amount;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let db = Database::filesystem("./db");
|
||||||
|
let p = db.save(Person::new_id("myperson", "first", "last"));
|
||||||
|
let p2 = db.save(Person::new_id("secperson", "second", "last"));
|
||||||
|
let banker = db.save(Person::new_id("banker", "boss", "bank"));
|
||||||
|
|
||||||
|
lend_money(&p, &banker, 250.0, &db);
|
||||||
|
|
||||||
|
lend_money(&p2, &p, 100.0, &db);
|
||||||
|
|
||||||
|
lend_money(&p2, &banker, 150.0, &db);
|
||||||
|
|
||||||
|
let financer: Vec<_> = LoanRelation::get_payer_of(&p2, &db)
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| LoanRelation::payer(&x.dereference(&db), &db))
|
||||||
|
.map(|x| x.read().first_name.current().cloned().unwrap())
|
||||||
|
.collect();
|
||||||
|
println!(
|
||||||
|
"{} is financed by {:?}",
|
||||||
|
p2.read().first_name.current().unwrap(),
|
||||||
|
financer
|
||||||
|
);
|
||||||
|
|
||||||
|
let brokers = LoanRelation::get_receiver_of(&p, &db);
|
||||||
|
dbg!(&brokers);
|
||||||
|
let brokers = dereference(&brokers, &db);
|
||||||
|
|
||||||
|
brokers.iter().for_each(|x| println!("{:?}", x.read()));
|
||||||
|
|
||||||
|
pay_off(&p2, &p, 100.0, &db);
|
||||||
|
|
||||||
|
let brokers = dereference(&LoanRelation::get_receiver_of(&p, &db), &db);
|
||||||
|
brokers.iter().for_each(|x| println!("{:?}", x.read()));
|
||||||
|
}
|
29
examples/parent.rs
Normal file
29
examples/parent.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use owl::{db::model::person::Person, prelude::*};
|
||||||
|
|
||||||
|
#[relation("parent", Person, "child", Person, RelationKind::Unidirectional)]
|
||||||
|
pub struct ParentRelation;
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
let db = Database::in_memory();
|
||||||
|
let p = Person::new_id("myperson", "first", "last");
|
||||||
|
db.save(p);
|
||||||
|
let p: Model<Person> = db.get("myperson").unwrap();
|
||||||
|
|
||||||
|
let p2 = Person::new_id("secperson", "second", "last");
|
||||||
|
let p2 = db.save(p2);
|
||||||
|
|
||||||
|
ParentRelation.add(&p, &p2, None, &db);
|
||||||
|
|
||||||
|
let children_of = ParentRelation::get_child_of(&p, &db);
|
||||||
|
dbg!(&children_of);
|
||||||
|
|
||||||
|
let children_of = dereference(&children_of, &db);
|
||||||
|
children_of
|
||||||
|
.iter()
|
||||||
|
.for_each(|x| println!("child: {:?}", x.read()));
|
||||||
|
|
||||||
|
let my_parents = dereference(&ParentRelation::get_parent_of(&p2, &db), &db);
|
||||||
|
my_parents
|
||||||
|
.iter()
|
||||||
|
.for_each(|x| println!("parent: {:?}", x.read()));
|
||||||
|
}
|
61
examples/references.rs
Normal file
61
examples/references.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use owl::{db::model::person::Person, prelude::*};
|
||||||
|
|
||||||
|
#[model]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Car {
|
||||||
|
pub id: Id,
|
||||||
|
pub price: u32,
|
||||||
|
pub driver: IdRef<Person>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
// Init
|
||||||
|
let db = Database::in_memory();
|
||||||
|
|
||||||
|
let per = Person::new_id("perso", "P1", "");
|
||||||
|
let per2: Person = Person::new_id("perso2", "P2", "");
|
||||||
|
|
||||||
|
// Save
|
||||||
|
let car = Car {
|
||||||
|
id: Id::new_ulid(),
|
||||||
|
price: 1000,
|
||||||
|
driver: per.reference(),
|
||||||
|
};
|
||||||
|
db.save(per);
|
||||||
|
let per2 = db.save(per2);
|
||||||
|
let mut car = db.save(car);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"P1 has {} cars",
|
||||||
|
db.query(|car: &Car| {
|
||||||
|
car.driver
|
||||||
|
.dereference(&db)
|
||||||
|
.read()
|
||||||
|
.first_name
|
||||||
|
.current()
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
== "P1"
|
||||||
|
})
|
||||||
|
.iter()
|
||||||
|
.count()
|
||||||
|
);
|
||||||
|
|
||||||
|
car.write(&db, |car| {
|
||||||
|
car.driver = per2.reference();
|
||||||
|
});
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"P1 has {} cars",
|
||||||
|
db.query(|car: &Car| {
|
||||||
|
car.driver
|
||||||
|
.try_dereference(&db)
|
||||||
|
.map(|x| x.read().first_name.current().unwrap().as_str() == "P1")
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.iter()
|
||||||
|
.count()
|
||||||
|
);
|
||||||
|
}
|
43
examples/stock.rs
Normal file
43
examples/stock.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use owl::prelude::*;
|
||||||
|
|
||||||
|
#[model]
|
||||||
|
pub struct Stock {
|
||||||
|
pub id: Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[model]
|
||||||
|
pub struct Owner {
|
||||||
|
pub id: Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relation("owner", Owner, "stock", Stock, RelationKind::Unidirectional)]
|
||||||
|
pub struct StockOrder {
|
||||||
|
amount: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let db = Database::filesystem("./db");
|
||||||
|
|
||||||
|
let o = Owner { id: Id::new_ulid() };
|
||||||
|
let o = db.save(o);
|
||||||
|
|
||||||
|
let apl = Stock {
|
||||||
|
id: Id::String("APL".to_string()),
|
||||||
|
};
|
||||||
|
let apl = db.save(apl);
|
||||||
|
|
||||||
|
StockOrder { amount: 1.0 }.add(&o, &apl, None, &db);
|
||||||
|
|
||||||
|
for order in StockOrder::get_stock_of(&o, &db) {
|
||||||
|
let rel = db.get(order).unwrap();
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{} has {} {}",
|
||||||
|
StockOrder::owner(&rel, &db).read().id.to_string(),
|
||||||
|
StockOrder::meta(&rel).unwrap().amount,
|
||||||
|
StockOrder::stock(&rel, &db).read().id.to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
61
examples/watch.rs
Normal file
61
examples/watch.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use owl::prelude::*;
|
||||||
|
|
||||||
|
#[model]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Contact {
|
||||||
|
pub id: Id,
|
||||||
|
pub age: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn age_one_year(p: &mut Model<Contact>, db: &Database) {
|
||||||
|
p.write(db, |p| p.age += 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
// Init
|
||||||
|
let db = Database::in_memory();
|
||||||
|
|
||||||
|
let mut c1 = db.save(Contact {
|
||||||
|
id: Id::String("c1".to_string()),
|
||||||
|
age: 15,
|
||||||
|
});
|
||||||
|
let mut c2 = db.save(Contact {
|
||||||
|
id: Id::String("c2".to_string()),
|
||||||
|
age: 17,
|
||||||
|
});
|
||||||
|
let mut c3 = db.save(Contact {
|
||||||
|
id: Id::String("c3".to_string()),
|
||||||
|
age: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
let age_18_watcher = db.watch::<Contact, _, _>(
|
||||||
|
|c| c.age == 18,
|
||||||
|
|c| {
|
||||||
|
println!("{} turned 18", c.id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
std::thread::spawn(move || loop {
|
||||||
|
age_18_watcher.process();
|
||||||
|
});
|
||||||
|
|
||||||
|
age_one_year(&mut c1, &db);
|
||||||
|
age_one_year(&mut c2, &db);
|
||||||
|
age_one_year(&mut c3, &db);
|
||||||
|
|
||||||
|
age_one_year(&mut c1, &db);
|
||||||
|
age_one_year(&mut c2, &db);
|
||||||
|
age_one_year(&mut c3, &db);
|
||||||
|
|
||||||
|
age_one_year(&mut c1, &db);
|
||||||
|
age_one_year(&mut c2, &db);
|
||||||
|
age_one_year(&mut c3, &db);
|
||||||
|
|
||||||
|
age_one_year(&mut c1, &db);
|
||||||
|
age_one_year(&mut c2, &db);
|
||||||
|
age_one_year(&mut c3, &db);
|
||||||
|
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(10));
|
||||||
|
}
|
70
owl_macro/Cargo.lock
generated
Normal file
70
owl_macro/Cargo.lock
generated
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert_case"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "owl_macro"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"convert_case",
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.95"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
14
owl_macro/Cargo.toml
Normal file
14
owl_macro/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "owl_macro"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = "1"
|
||||||
|
syn = { version = "2", features = ["full"] }
|
||||||
|
proc-macro2 = "1"
|
||||||
|
convert_case = "0.6"
|
||||||
|
heck = "0.5.0"
|
215
owl_macro/src/lib.rs
Normal file
215
owl_macro/src/lib.rs
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
extern crate proc_macro;
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use heck::ToSnakeCase;
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{parse::{Parse, ParseStream}, parse_macro_input, ItemStruct, LitStr, Path, Token};
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
/// Create a relation.
|
||||||
|
///
|
||||||
|
/// Syntax: `#[relation("TopName", Top, "SubName", Sub, RelationKind::*)]`
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// #[relation("Lover", Person, "Loved", Person, RelationKind::Bidirectional)]
|
||||||
|
/// pub struct LoveRelation {}
|
||||||
|
/// ```
|
||||||
|
pub fn relation(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
|
let args = parse_macro_input!(args as RelationArgs);
|
||||||
|
let input_struct = parse_macro_input!(input as ItemStruct);
|
||||||
|
|
||||||
|
let struct_name = &input_struct.ident;
|
||||||
|
let role1 = &args.role1;
|
||||||
|
let role1_fn = format_ident!("get_{}_of", &args.role2);
|
||||||
|
let ty1 = &args.ty1;
|
||||||
|
let role2 = &args.role2;
|
||||||
|
let role2_fn = format_ident!("get_{}_of", &args.role1);
|
||||||
|
let ty2 = &args.ty2;
|
||||||
|
let kind = &args.kind;
|
||||||
|
|
||||||
|
let struct_ref_name = format_ident!("{}Reference", struct_name);
|
||||||
|
|
||||||
|
let ref_name = format!(
|
||||||
|
"{}-refs",
|
||||||
|
format_ident!("{}", struct_name).to_string().to_snake_case()
|
||||||
|
);
|
||||||
|
|
||||||
|
let fns = match quote!(#kind).to_string().as_str() {
|
||||||
|
"RelationKind :: Bidirectional" => { quote! {
|
||||||
|
pub fn #role1_fn(#role1: &owl::db::Model<#ty1>, db: &owl::db::Database) -> Vec<owl::db::relation::IdRef<#struct_ref_name>> {
|
||||||
|
Self::relation().get_all_top(#role1, &*db.storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn #role2_fn(#role2: &owl::db::Model<#ty2>, db: &owl::db::Database) -> Vec<owl::db::relation::IdRef<#struct_ref_name>> {
|
||||||
|
Self::relation().get_all_sub(#role2, &*db.storage)
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
"RelationKind :: Unidirectional" => { quote! {
|
||||||
|
pub fn #role1_fn<X: Into<owl::db::relation::IdRef<#ty1>>>(#role1: X, db: &owl::db::Database) -> Vec<owl::db::relation::IdRef<#struct_ref_name>> {
|
||||||
|
Self::relation().get_all_with_t(#role1, &*db.storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn #role2_fn<X: Into<owl::db::relation::IdRef<#ty2>>>(#role2: X, db: &owl::db::Database) -> Vec<owl::db::relation::IdRef<#struct_ref_name>> {
|
||||||
|
Self::relation().get_all_with_s(#role2, &*db.storage)
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
_ => { unimplemented!() }
|
||||||
|
};
|
||||||
|
|
||||||
|
let relation_name = struct_name.to_string().to_case(Case::Snake);
|
||||||
|
|
||||||
|
let expanded = quote! {
|
||||||
|
#[derive(owl::Serialize, owl::Deserialize, Default)]
|
||||||
|
#input_struct
|
||||||
|
|
||||||
|
#[derive(owl::Deserialize, owl::Serialize, Debug)]
|
||||||
|
pub struct #struct_ref_name {
|
||||||
|
pub id: Id,
|
||||||
|
pub inner: owl::db::relation::RelationReference
|
||||||
|
}
|
||||||
|
|
||||||
|
impl owl::Identifiable for #struct_ref_name {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
Id::String(format!("[{}->{}]", self.inner.top.id, self.inner.sub.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn model_id() -> String {
|
||||||
|
#ref_name.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #struct_ref_name {
|
||||||
|
pub fn alter_meta<F: Fn(&mut #struct_name)>(
|
||||||
|
&mut self,
|
||||||
|
u: F
|
||||||
|
) {
|
||||||
|
self.inner.alter_meta(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl owl::db::relation::RelationRef for #struct_ref_name {
|
||||||
|
fn top(&self) -> IdRef<serde_json::Value> {
|
||||||
|
self.inner.top.clone()
|
||||||
|
}
|
||||||
|
fn sub(&self) -> IdRef<serde_json::Value> {
|
||||||
|
self.inner.sub.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_ref(r: owl::db::relation::RelationReference) -> Self {
|
||||||
|
Self {
|
||||||
|
id: owl::db::id::Id::String(r.id()),
|
||||||
|
inner: r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_meta(&mut self, weight: Option<f64>, meta: Option<serde_json::Value>) {
|
||||||
|
self.inner.weight = weight;
|
||||||
|
self.inner.meta = meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #struct_name {
|
||||||
|
pub fn relation() -> Relation<#ty1, #ty2, #struct_ref_name, #kind> {
|
||||||
|
owl::db::relation::Relation::new(#relation_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#fns
|
||||||
|
|
||||||
|
pub fn meta(reference: &Model<#struct_ref_name>) -> Option<Self> {
|
||||||
|
if let Some(meta) = reference.read().inner.meta.clone() {
|
||||||
|
return Some(serde_json::from_value(meta).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn #role1(reference: &Model<#struct_ref_name>, db: &owl::db::Database) -> Model<#ty1> {
|
||||||
|
unsafe { owl::db::relation::Relation::<#ty1, #ty2, #struct_ref_name, #kind>::top(reference, &db) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn #role2(reference: &Model<#struct_ref_name>, db: &owl::db::Database) -> Model<#ty2> {
|
||||||
|
unsafe { owl::db::relation::Relation::<#ty1, #ty2, #struct_ref_name, #kind>::sub(reference, &db) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(#role1: &owl::db::Model<#ty1>, #role2: &owl::db::Model<#ty2>, db: &owl::db::Database) -> Option<Model<#struct_ref_name>> {
|
||||||
|
unsafe { Self::relation().get(#role1, #role2, &db) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&self, #role1: &owl::db::Model<#ty1>, #role2: &owl::db::Model<#ty2>, weight: Option<f64>, db: &owl::db::Database) {
|
||||||
|
let meta = serde_json::to_value(self.clone()).ok();
|
||||||
|
Self::relation().add(#role1, #role2, weight, meta, &db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TokenStream::from(expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
/// Create a model.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// #[model]
|
||||||
|
/// pub struct MyTing {
|
||||||
|
/// pub id: Id
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn model(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
|
let input_struct = parse_macro_input!(input as ItemStruct);
|
||||||
|
|
||||||
|
let struct_name = &input_struct.ident;
|
||||||
|
|
||||||
|
let relation_name = struct_name.to_string().to_case(Case::Snake);
|
||||||
|
|
||||||
|
let expanded = quote! {
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
#input_struct
|
||||||
|
impl owl::db::store::Saveable for #struct_name {}
|
||||||
|
|
||||||
|
impl owl::Identifiable for #struct_name {
|
||||||
|
fn id(&self) -> owl::db::id::Id {
|
||||||
|
self.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn model_id() -> String {
|
||||||
|
#relation_name.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TokenStream::from(expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RelationArgs {
|
||||||
|
role1: syn::Ident,
|
||||||
|
ty1: syn::Path,
|
||||||
|
role2: syn::Ident,
|
||||||
|
ty2: syn::Path,
|
||||||
|
kind: syn::Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for RelationArgs {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
let role1: LitStr = input.parse()?;
|
||||||
|
input.parse::<Token![,]>()?;
|
||||||
|
let ty1: Path = input.parse()?;
|
||||||
|
input.parse::<Token![,]>()?;
|
||||||
|
let role2: LitStr = input.parse()?;
|
||||||
|
input.parse::<Token![,]>()?;
|
||||||
|
let ty2: Path = input.parse()?;
|
||||||
|
input.parse::<Token![,]>()?;
|
||||||
|
let kind: Path = input.parse()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
role1: syn::Ident::new(&role1.value(), role1.span()),
|
||||||
|
ty1,
|
||||||
|
role2: syn::Ident::new(&role2.value(), role2.span()),
|
||||||
|
ty2,
|
||||||
|
kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
59
src/cli.rs
Normal file
59
src/cli.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
use argh::FromArgs;
|
||||||
|
|
||||||
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
|
/// owl cli
|
||||||
|
pub struct OwlCLI {
|
||||||
|
#[argh(subcommand)]
|
||||||
|
pub nested: OwlCLICommands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
|
#[argh(subcommand)]
|
||||||
|
pub enum OwlCLICommands {
|
||||||
|
List(ListCommand),
|
||||||
|
Get(GetCommand),
|
||||||
|
Store(StoreCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
|
/// First subcommand.
|
||||||
|
#[argh(subcommand, name = "list")]
|
||||||
|
pub struct ListCommand {
|
||||||
|
#[argh(option)]
|
||||||
|
/// database
|
||||||
|
pub db: Option<String>,
|
||||||
|
|
||||||
|
#[argh(positional)]
|
||||||
|
/// collection
|
||||||
|
pub collection: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
|
/// First subcommand.
|
||||||
|
#[argh(subcommand, name = "store")]
|
||||||
|
pub struct StoreCommand {
|
||||||
|
#[argh(option)]
|
||||||
|
/// database
|
||||||
|
pub db: Option<String>,
|
||||||
|
|
||||||
|
#[argh(positional)]
|
||||||
|
/// collection
|
||||||
|
pub collection: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
|
/// Second subcommand.
|
||||||
|
#[argh(subcommand, name = "get")]
|
||||||
|
pub struct GetCommand {
|
||||||
|
#[argh(option)]
|
||||||
|
/// database
|
||||||
|
pub db: Option<String>,
|
||||||
|
|
||||||
|
#[argh(positional)]
|
||||||
|
/// collection
|
||||||
|
pub collection: String,
|
||||||
|
|
||||||
|
#[argh(positional)]
|
||||||
|
/// id
|
||||||
|
pub id: String,
|
||||||
|
}
|
102
src/db/field.rs
Normal file
102
src/db/field.rs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
|
||||||
|
// historic field
|
||||||
|
#[derive(Deserialize, Serialize, Default, Debug, PartialEq)]
|
||||||
|
pub struct Historic<T> {
|
||||||
|
pub values: Vec<(chrono::DateTime<Utc>, T)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + Default> Historic<T> {
|
||||||
|
pub fn alter_or_default<F: Fn(&mut T)>(&mut self, f: F) {
|
||||||
|
let mut c = if let Some(c) = self.current().cloned() {
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
T::default()
|
||||||
|
};
|
||||||
|
f(&mut c);
|
||||||
|
self.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Deref for Historic<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.current().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Historic<T> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { values: vec![] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current(&self) -> Option<&T> {
|
||||||
|
let x = self.values.iter().max_by_key(|(dt, _)| *dt)?;
|
||||||
|
|
||||||
|
Some(&x.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn alter<F: Fn(&mut T)>(&mut self, f: F) {
|
||||||
|
let mut c = self.current().unwrap().clone();
|
||||||
|
f(&mut c);
|
||||||
|
self.add(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, value: T) {
|
||||||
|
self.values.push((chrono::Utc::now(), value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Historicable: Sized {
|
||||||
|
fn historic(self) -> Historic<Self> {
|
||||||
|
Historic {
|
||||||
|
values: vec![(chrono::Utc::now(), self)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Historicable for String {}
|
||||||
|
|
||||||
|
/// a unique `Vec<T>`
|
||||||
|
pub struct UniVec<T>(Vec<T>);
|
||||||
|
|
||||||
|
impl<T: PartialEq> UniVec<T> {
|
||||||
|
pub fn push(&mut self, val: T) -> bool {
|
||||||
|
if self.0.iter().any(|x| *x == val) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.0.push(val);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Serialize> Serialize for UniVec<T> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
Vec::<T>::serialize(&self.0, serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de, T: Deserialize<'de>> Deserialize<'de> for UniVec<T> {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = Vec::deserialize(deserializer)?;
|
||||||
|
Ok(Self(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for UniVec<T> {
|
||||||
|
type Target = Vec<T>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
63
src/db/id.rs
Normal file
63
src/db/id.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
/// Database IDs
|
||||||
|
pub enum Id {
|
||||||
|
/// String ID
|
||||||
|
String(String),
|
||||||
|
/// ULID
|
||||||
|
ULID(ulid::Ulid),
|
||||||
|
/// UUID
|
||||||
|
UUID(uuid::Uuid),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Id {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
match s.parse::<ulid::Ulid>() {
|
||||||
|
Ok(ulid) => Ok(Id::ULID(ulid)),
|
||||||
|
Err(_) => match s.parse::<uuid::Uuid>() {
|
||||||
|
Ok(uuid) => Ok(Id::UUID(uuid)),
|
||||||
|
Err(_) => Ok(Id::String(s)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Id {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Id::String(str) => str.serialize(serializer),
|
||||||
|
Id::ULID(ulid) => ulid.serialize(serializer),
|
||||||
|
Id::UUID(uuid) => uuid.serialize(serializer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Id {
|
||||||
|
/// Generate a new random ULID based ID
|
||||||
|
pub fn new_ulid() -> Self {
|
||||||
|
Self::ULID(ulid::Ulid::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Id {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Id::String(s) => s.clone(),
|
||||||
|
Id::ULID(ulid) => ulid.to_string(),
|
||||||
|
Id::UUID(uuid) => uuid.to_string(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
394
src/db/mod.rs
Normal file
394
src/db/mod.rs
Normal file
|
@ -0,0 +1,394 @@
|
||||||
|
use parking_lot::{RwLock, RwLockReadGuard};
|
||||||
|
use std::{
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use model::file::File;
|
||||||
|
use relation::{dereference, IdRef};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use store::Store;
|
||||||
|
|
||||||
|
use crate::Identifiable;
|
||||||
|
|
||||||
|
pub mod field;
|
||||||
|
pub mod id;
|
||||||
|
pub mod model;
|
||||||
|
pub mod relation;
|
||||||
|
pub mod store;
|
||||||
|
|
||||||
|
/// really simple query function. it iterates over every object and matches against `predicate`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// let queried = query_t(|x: &MyType| {
|
||||||
|
/// x.id.to_string().starts_with("something")
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
pub fn query_t<
|
||||||
|
T: Identifiable + Serialize + Send + Sync + for<'a> Deserialize<'a> + 'static,
|
||||||
|
F: Fn(&T) -> bool,
|
||||||
|
>(
|
||||||
|
predicate: F,
|
||||||
|
db: &Database,
|
||||||
|
) -> Vec<Model<T>> {
|
||||||
|
let collection = T::model_id();
|
||||||
|
let all = Store::get_ids(&collection, &*db.storage);
|
||||||
|
let all: Vec<IdRef<T>> = all
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| IdRef::new(format!("{collection}::{x}")))
|
||||||
|
.collect();
|
||||||
|
let all = dereference(&all, db);
|
||||||
|
all.into_iter()
|
||||||
|
.filter(|x| {
|
||||||
|
let r = x.read();
|
||||||
|
let rf = r.deref();
|
||||||
|
(predicate)(rf)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Database Instance
|
||||||
|
pub struct Database {
|
||||||
|
/// Underlying storage layer
|
||||||
|
pub storage: Arc<dyn vfs::FileSystem>,
|
||||||
|
/// Write models with schema
|
||||||
|
pub named: bool,
|
||||||
|
/// Use memory caching
|
||||||
|
pub cached: bool,
|
||||||
|
/// Memory Cache
|
||||||
|
records: Arc<DashMap<String, DashMap<String, Box<dyn ModelObj>>>>,
|
||||||
|
watcher: Arc<DashMap<String, Vec<crossbeam::channel::Sender<Box<dyn ModelObj>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ModelObj: Send + Sync {
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ModelWatcher<T> {
|
||||||
|
filter: Box<dyn Fn(&T) -> bool + 'static + Send + Sync>,
|
||||||
|
apply: Box<dyn Fn(&T) + 'static + Send + Sync>,
|
||||||
|
recv: crossbeam::channel::Receiver<Box<dyn ModelObj>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static + Identifiable> ModelWatcher<T> {
|
||||||
|
pub fn process(&self) {
|
||||||
|
if let Ok(val) = self.recv.recv() {
|
||||||
|
self.handle(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recv(&self) -> Model<T> {
|
||||||
|
let res = self.recv.recv().unwrap();
|
||||||
|
res.as_any().downcast_ref::<Model<T>>().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(&self, val: Box<dyn ModelObj>) {
|
||||||
|
log::trace!("Received watch event");
|
||||||
|
let model: &Model<T> = val.as_any().downcast_ref().unwrap();
|
||||||
|
if (self.filter)(&model.read()) {
|
||||||
|
log::info!("Watcher processing {}", model.full_id());
|
||||||
|
(self.apply)(&model.read());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_process(&self) {
|
||||||
|
if let Ok(val) = self.recv.recv_timeout(std::time::Duration::from_secs(1)) {
|
||||||
|
self.handle(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for Database {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
storage: Arc::clone(&self.storage),
|
||||||
|
named: self.named.clone(),
|
||||||
|
cached: self.cached.clone(),
|
||||||
|
records: Arc::clone(&self.records),
|
||||||
|
watcher: Arc::clone(&self.watcher),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
/// Create a new in memory database
|
||||||
|
pub fn in_memory() -> Self {
|
||||||
|
Self::ensure_init(Self {
|
||||||
|
storage: Arc::new(vfs::MemoryFS::new()),
|
||||||
|
cached: false,
|
||||||
|
named: false,
|
||||||
|
records: Arc::new(DashMap::new()),
|
||||||
|
watcher: Arc::new(DashMap::new()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn watch<T, F, G>(&self, filter: F, then: G) -> ModelWatcher<T>
|
||||||
|
where
|
||||||
|
T: Identifiable + 'static + Send + Sync,
|
||||||
|
F: Fn(&T) -> bool + 'static + Send + Sync,
|
||||||
|
G: Fn(&T) + 'static + Send + Sync,
|
||||||
|
{
|
||||||
|
let id = T::model_id();
|
||||||
|
|
||||||
|
let (send, recv) = crossbeam::channel::unbounded();
|
||||||
|
|
||||||
|
self.watcher.entry(id).or_default().push(send);
|
||||||
|
|
||||||
|
ModelWatcher {
|
||||||
|
filter: Box::new(filter),
|
||||||
|
apply: Box::new(then),
|
||||||
|
recv,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_id<T: for<'b> Deserialize<'b>>(&self, collection: &str, id: &str) -> T {
|
||||||
|
unsafe { Store::get(collection, id, &*self.storage).unwrap() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn named(mut self) -> Self {
|
||||||
|
self.named = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_cache(mut self, choice: bool) -> Self {
|
||||||
|
self.cached = choice;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query `Vec<T>` which match `predicate`
|
||||||
|
pub fn query<
|
||||||
|
T: Identifiable + for<'b> Deserialize<'b> + Serialize + Send + Sync + 'static,
|
||||||
|
F: Fn(&T) -> bool,
|
||||||
|
>(
|
||||||
|
&self,
|
||||||
|
predicate: F,
|
||||||
|
) -> Vec<Model<T>> {
|
||||||
|
query_t(predicate, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update every model in `entries` according to `u(_)`
|
||||||
|
pub fn update<T: 'static + Serialize + Identifiable + Send + Sync, F: Fn(&mut T)>(
|
||||||
|
&self,
|
||||||
|
entries: &mut [Model<T>],
|
||||||
|
u: F,
|
||||||
|
) {
|
||||||
|
for e in entries {
|
||||||
|
e.write(self, &u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_init(db: Self) -> Self {
|
||||||
|
let _ = db.storage.create_dir("/collection");
|
||||||
|
let _ = db.storage.create_dir("/files");
|
||||||
|
db
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new fs backed database at `path`
|
||||||
|
pub fn filesystem(path: &str) -> Self {
|
||||||
|
Self::ensure_init(Self {
|
||||||
|
storage: Arc::new(vfs::PhysicalFS::new(path)),
|
||||||
|
named: false,
|
||||||
|
cached: true,
|
||||||
|
records: Arc::new(DashMap::new()),
|
||||||
|
watcher: Arc::new(DashMap::new()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all collections
|
||||||
|
pub fn list(&self) -> Vec<String> {
|
||||||
|
Store::list(&*self.storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all entries in `collection`
|
||||||
|
pub fn list_entries(&self, collection: &str) -> Vec<String> {
|
||||||
|
Store::get_ids(collection, &*self.storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_cached<T: Identifiable + for<'b> Deserialize<'b> + 'static>(
|
||||||
|
&self,
|
||||||
|
col: String,
|
||||||
|
id: String,
|
||||||
|
) -> Option<Model<T>> {
|
||||||
|
// let id = id.trim_start_matches(&format!("{}::", T::model_id()));
|
||||||
|
let entry = self.records.get(&col)?;
|
||||||
|
let entry = entry.get(&id)?;
|
||||||
|
let model: &Model<T> = entry.value().as_any().downcast_ref()?;
|
||||||
|
Some(model.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_cache<T: Serialize + Identifiable + Send + Sync + 'static>(
|
||||||
|
&self,
|
||||||
|
id: String,
|
||||||
|
model: Model<T>,
|
||||||
|
) {
|
||||||
|
self.records
|
||||||
|
.entry(T::model_id())
|
||||||
|
.or_default()
|
||||||
|
.insert(id, Box::new(model.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a model `T` from `id`
|
||||||
|
pub fn get<
|
||||||
|
T: Identifiable + Serialize + Send + Sync + for<'b> Deserialize<'b> + 'static,
|
||||||
|
I: Into<IdRef<T>>,
|
||||||
|
>(
|
||||||
|
&self,
|
||||||
|
id: I,
|
||||||
|
) -> Option<Model<T>> {
|
||||||
|
let id: IdRef<T> = id.into();
|
||||||
|
let col = parse_collection(&id.id).unwrap_or(T::model_id());
|
||||||
|
log::trace!("Getting {id} as {col}");
|
||||||
|
if self.cached {
|
||||||
|
if let Some(cached) = self.try_cached(col, id.to_string()) {
|
||||||
|
log::trace!("Returning {id} from cache");
|
||||||
|
return Some(cached);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!("Trying to load {id}");
|
||||||
|
let col = parse_collection(&id.to_string());
|
||||||
|
let col = col.unwrap_or(T::model_id());
|
||||||
|
let m: T = unsafe {
|
||||||
|
Store::get(
|
||||||
|
&col,
|
||||||
|
&id.id.trim_start_matches(&format!("{}::", col)),
|
||||||
|
&*self.storage,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
let model = Model {
|
||||||
|
inner: Arc::new(RwLock::new(m)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.cached {
|
||||||
|
self.save_cache(id.to_string(), model.clone());
|
||||||
|
log::trace!("Returning {id} and saved to cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save a raw model into the database
|
||||||
|
pub unsafe fn save_raw<T: Serialize + Identifiable>(&self, data: &T, collection: &str) {
|
||||||
|
Store::save(collection, data, self.named, &*self.storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_model<T: Serialize + Identifiable + Send + Sync + 'static>(&self, data: &Model<T>) {
|
||||||
|
if let Some(watchers) = self.watcher.get(&T::model_id()) {
|
||||||
|
log::trace!("Notifying watchers");
|
||||||
|
for watch in watchers.iter() {
|
||||||
|
watch.send(Box::new(data.clone())).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = data.read();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
Store::save(&T::model_id(), data.deref(), self.named, &*self.storage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save a model `T` into the database
|
||||||
|
pub fn save<'a, T: Serialize + Identifiable + Send + Sync + 'static>(
|
||||||
|
&'a self,
|
||||||
|
data: T,
|
||||||
|
) -> Model<T> {
|
||||||
|
let model = Model {
|
||||||
|
inner: Arc::new(RwLock::new(data)),
|
||||||
|
};
|
||||||
|
self.save_model(&model);
|
||||||
|
let id = model.full_id().to_string();
|
||||||
|
|
||||||
|
if self.cached {
|
||||||
|
log::trace!("Saving {id} to cache");
|
||||||
|
self.save_cache(id, model.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : cache eviction based on ref counts
|
||||||
|
|
||||||
|
pub struct Model<T> {
|
||||||
|
inner: Arc<RwLock<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model<File> {
|
||||||
|
pub fn read_file(&self, db: &Database) -> Vec<u8> {
|
||||||
|
self.read().read(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Identifiable> Model<T> {
|
||||||
|
pub fn from_raw(raw: T) -> Model<T> {
|
||||||
|
Model {
|
||||||
|
inner: Arc::new(RwLock::new(raw)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn full_id(&self) -> String {
|
||||||
|
self.read().full_id()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reference(&self) -> IdRef<T> {
|
||||||
|
self.read().reference()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(&self) -> RwLockReadGuard<T> {
|
||||||
|
self.inner.read()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static + Serialize + Identifiable + Send + Sync> Model<T> {
|
||||||
|
pub fn write<F: Fn(&mut T)>(&mut self, db: &Database, u: F) {
|
||||||
|
let mut me = self.inner.write();
|
||||||
|
u(me.deref_mut());
|
||||||
|
drop(me);
|
||||||
|
db.save_model(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the model inline without writing to `Database`
|
||||||
|
pub fn write_raw_inline<F: Fn(&mut T)>(&mut self, u: F) {
|
||||||
|
let mut me = self.inner.write();
|
||||||
|
u(me.deref_mut());
|
||||||
|
drop(me);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static + Serialize + Identifiable + Send + Sync> ModelObj for Model<T> {
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for Model<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::clone(&self.inner),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_collection(id: &str) -> Option<String> {
|
||||||
|
if id.starts_with("[") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let splitted: Vec<_> = id.split("[").collect();
|
||||||
|
if splitted.len() > 1 {
|
||||||
|
let cols = splitted.first().unwrap();
|
||||||
|
let col = cols.trim_end_matches("::");
|
||||||
|
return Some(col.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut splitted = id.split("::").collect::<Vec<_>>();
|
||||||
|
if splitted.len() == 1 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
splitted.pop().unwrap();
|
||||||
|
Some(splitted.join("::"))
|
||||||
|
}
|
||||||
|
}
|
98
src/db/model/file.rs
Normal file
98
src/db/model/file.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use std::{io::Write, process::Stdio};
|
||||||
|
|
||||||
|
use owl_macro::model;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
pub use crate as owl;
|
||||||
|
use crate::{
|
||||||
|
db::Database,
|
||||||
|
prelude::{Id, Saveable},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn sha256(input: &[u8]) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(input);
|
||||||
|
let result = hasher.finalize();
|
||||||
|
format!("{:x}", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A generic file
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[model]
|
||||||
|
pub struct File {
|
||||||
|
id: Id,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub size: usize,
|
||||||
|
pub mime: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mime_type(data: &[u8]) -> String {
|
||||||
|
let mut child = std::process::Command::new("file")
|
||||||
|
.arg("--brief")
|
||||||
|
.arg("--mime-type")
|
||||||
|
.arg("-") // Read from stdin
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let stdin = child.stdin.as_mut().ok_or("Failed to open stdin").unwrap();
|
||||||
|
stdin.write_all(data).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = child.wait_with_output().unwrap();
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
log::error!("Error getting MIME");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mime_type = String::from_utf8(output.stdout).unwrap().trim().to_string();
|
||||||
|
mime_type
|
||||||
|
}
|
||||||
|
|
||||||
|
impl File {
|
||||||
|
pub fn new(data: Vec<u8>, name: Option<String>, db: &Database) -> Self {
|
||||||
|
let hash = sha256(&data);
|
||||||
|
|
||||||
|
let f = Self {
|
||||||
|
id: Id::String(hash.clone()),
|
||||||
|
size: data.len(),
|
||||||
|
mime: get_mime_type(&data),
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
|
||||||
|
let first: String = hash.chars().take(2).collect();
|
||||||
|
let second: String = hash.chars().skip(2).take(2).collect();
|
||||||
|
|
||||||
|
let _ = db.storage.create_dir(&format!("/files/{first}"));
|
||||||
|
let _ = db.storage.create_dir(&format!("/files/{first}/{second}"));
|
||||||
|
db.storage
|
||||||
|
.create_file(&format!("/files/{first}/{second}/{hash}"))
|
||||||
|
.unwrap()
|
||||||
|
.write(&data)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
f.save(false, &*db.storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
f
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(&self, db: &Database) -> Vec<u8> {
|
||||||
|
let hash = &self.id.to_string();
|
||||||
|
let first: String = hash.chars().take(2).collect();
|
||||||
|
let second: String = hash.chars().skip(2).take(2).collect();
|
||||||
|
|
||||||
|
let mut buf = Vec::with_capacity(self.size as usize);
|
||||||
|
|
||||||
|
db.storage
|
||||||
|
.open_file(&format!("/files/{first}/{second}/{hash}"))
|
||||||
|
.unwrap()
|
||||||
|
.read_to_end(&mut buf)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
}
|
36
src/db/model/location.rs
Normal file
36
src/db/model/location.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use owl_macro::model;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub use crate as owl;
|
||||||
|
|
||||||
|
use crate::db::id::Id;
|
||||||
|
|
||||||
|
/// Represents a geographical location with an ID, coordinates, and optional address
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
#[model]
|
||||||
|
pub struct Location {
|
||||||
|
pub id: Id,
|
||||||
|
/// A common name for the `Location`
|
||||||
|
pub location_name: Option<String>,
|
||||||
|
/// Geographical coordinates of the location
|
||||||
|
pub geo: (f64, f64),
|
||||||
|
/// The street address
|
||||||
|
pub location_address: Option<String>,
|
||||||
|
/// The city name
|
||||||
|
pub city: Option<String>,
|
||||||
|
/// The state or province name
|
||||||
|
pub location_state: Option<String>,
|
||||||
|
/// The postal or ZIP code
|
||||||
|
pub postal_code: Option<String>,
|
||||||
|
/// The country name
|
||||||
|
pub country: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents geographical coordinates with latitude and longitude
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct GeoCoordinates {
|
||||||
|
/// The latitude coordinate
|
||||||
|
pub latitude: f64,
|
||||||
|
/// The longitude coordinate
|
||||||
|
pub longitude: f64,
|
||||||
|
}
|
17
src/db/model/mail_address.rs
Normal file
17
src/db/model/mail_address.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use owl_macro::model;
|
||||||
|
|
||||||
|
pub use crate as owl;
|
||||||
|
|
||||||
|
use crate::db::id::Id;
|
||||||
|
|
||||||
|
/// Represents a mail address with an ID and the address itself
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[model]
|
||||||
|
pub struct MailAddress {
|
||||||
|
/// Unique identifier for the mail address
|
||||||
|
pub id: Id,
|
||||||
|
/// The actual mail address
|
||||||
|
pub mail_address: String,
|
||||||
|
// The domain
|
||||||
|
pub domain: String,
|
||||||
|
}
|
6
src/db/model/mod.rs
Normal file
6
src/db/model/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pub mod file;
|
||||||
|
pub mod location;
|
||||||
|
pub mod mail_address;
|
||||||
|
pub mod person;
|
||||||
|
pub mod phone_number;
|
||||||
|
pub mod vehicle;
|
94
src/db/model/person.rs
Normal file
94
src/db/model/person.rs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
use owl_macro::model;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub use crate as owl;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::{
|
||||||
|
field::{Historic, Historicable},
|
||||||
|
id::Id,
|
||||||
|
query_t,
|
||||||
|
relation::IdRef,
|
||||||
|
Database, Model,
|
||||||
|
},
|
||||||
|
Identifiable,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{location::Location, vehicle::Vehicle};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
#[model]
|
||||||
|
pub struct Person {
|
||||||
|
pub id: Id,
|
||||||
|
/// The person's first name
|
||||||
|
pub first_name: Historic<String>,
|
||||||
|
/// The person's last name
|
||||||
|
pub last_name: Historic<String>,
|
||||||
|
/// The person's date of birth.
|
||||||
|
pub date_of_birth: Option<chrono::NaiveDateTime>,
|
||||||
|
/// The person's gender.
|
||||||
|
pub gender: Option<Gender>,
|
||||||
|
/// Associated locations
|
||||||
|
pub locations: Historic<Vec<IdRef<Location>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||||
|
pub enum Gender {
|
||||||
|
Male,
|
||||||
|
Female,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Person {
|
||||||
|
pub fn new(first_name: &str, last_name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Id::new_ulid(),
|
||||||
|
first_name: first_name.to_string().historic(),
|
||||||
|
last_name: last_name.to_string().historic(),
|
||||||
|
date_of_birth: None,
|
||||||
|
gender: None,
|
||||||
|
locations: Historic::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_id(id: &str, first_name: &str, last_name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Id::String(id.to_string()),
|
||||||
|
first_name: first_name.to_string().historic(),
|
||||||
|
last_name: last_name.to_string().historic(),
|
||||||
|
date_of_birth: None,
|
||||||
|
gender: None,
|
||||||
|
locations: Historic::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vehicles(&self, db: &Database) -> Vec<Model<Vehicle>> {
|
||||||
|
query_t(
|
||||||
|
|vehicle: &Vehicle| {
|
||||||
|
if let Some(owner) = &vehicle.owned_by {
|
||||||
|
if *owner == self.reference() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
},
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub async fn remove_email(&self, email: uuid::Uuid) {
|
||||||
|
Relation::remove("relate_person_email", &self.id, &email).await
|
||||||
|
}pub async fn add_email(&self, email: uuid::Uuid) {
|
||||||
|
Relation::add("relate_person_email", &self.id, &email).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn emails(&self) -> Vec<MailAddress> {
|
||||||
|
Relation::find_relations("relate_person_email", "mail_address", &self.id).await
|
||||||
|
}
|
||||||
|
pub async fn phone_numbers(&self) -> Vec<PhoneNumber> {
|
||||||
|
Relation::find_relations("relate_person_phone_number", "phone_number", &self.id).await
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
14
src/db/model/phone_number.rs
Normal file
14
src/db/model/phone_number.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
use owl_macro::model;
|
||||||
|
|
||||||
|
pub use crate as owl;
|
||||||
|
|
||||||
|
use crate::db::id::Id;
|
||||||
|
|
||||||
|
/// Represents a phone number with an ID, the phone number itself, and the country code.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[model]
|
||||||
|
pub struct PhoneNumber {
|
||||||
|
id: Id,
|
||||||
|
number: String,
|
||||||
|
country_code: String,
|
||||||
|
}
|
16
src/db/model/vehicle.rs
Normal file
16
src/db/model/vehicle.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
use owl_macro::model;
|
||||||
|
|
||||||
|
pub use crate as owl;
|
||||||
|
|
||||||
|
use crate::db::{id::Id, relation::IdRef};
|
||||||
|
|
||||||
|
use super::person::Person;
|
||||||
|
|
||||||
|
/// A vehicle
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[model]
|
||||||
|
pub struct Vehicle {
|
||||||
|
pub id: Id,
|
||||||
|
pub owned_by: Option<IdRef<Person>>,
|
||||||
|
pub license_plate: Option<String>,
|
||||||
|
}
|
589
src/db/relation.rs
Normal file
589
src/db/relation.rs
Normal file
|
@ -0,0 +1,589 @@
|
||||||
|
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
use crate::Identifiable;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
id::Id,
|
||||||
|
store::{Saveable, Store},
|
||||||
|
Database, Model,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub mod RelationKind {
|
||||||
|
/// Unidirectional relation kind
|
||||||
|
///
|
||||||
|
/// The relation is the same for each node.
|
||||||
|
pub struct Unidirectional;
|
||||||
|
|
||||||
|
/// Unidirectional relation kind
|
||||||
|
///
|
||||||
|
/// Relations can be directed.
|
||||||
|
pub struct Bidirectional;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A relation between `T` and `S` with kind `U`.
|
||||||
|
/// Additionally with `weight` and `meta` values.
|
||||||
|
pub struct Relation<T, S, R, U> {
|
||||||
|
pub name: String,
|
||||||
|
pub top: std::marker::PhantomData<T>,
|
||||||
|
pub sub: std::marker::PhantomData<S>,
|
||||||
|
pub uni: std::marker::PhantomData<U>,
|
||||||
|
pub references: std::marker::PhantomData<R>,
|
||||||
|
pub weight: Option<f64>,
|
||||||
|
pub meta: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, S, R, U> Relation<T, S, R, U> {
|
||||||
|
/// Create a new relation with `name`
|
||||||
|
pub fn new(name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
weight: None,
|
||||||
|
meta: None,
|
||||||
|
references: std::marker::PhantomData,
|
||||||
|
top: std::marker::PhantomData,
|
||||||
|
sub: std::marker::PhantomData,
|
||||||
|
uni: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
/// A typed reference type
|
||||||
|
pub struct IdRef<T> {
|
||||||
|
pub id: String,
|
||||||
|
model: std::marker::PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Debug for IdRef<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_tuple("IdRef").field(&self.id).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for IdRef<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
id: self.id.clone(),
|
||||||
|
model: self.model.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl<T> Send for IdRef<T> {}
|
||||||
|
unsafe impl<T> Sync for IdRef<T> {}
|
||||||
|
|
||||||
|
impl<T> Display for IdRef<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&self.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: for<'a> Deserialize<'a>> From<String> for IdRef<T> {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
IdRef::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: for<'a> Deserialize<'a>> From<&str> for IdRef<T> {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
IdRef::new(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: for<'a> Deserialize<'a>> From<Id> for IdRef<T> {
|
||||||
|
fn from(value: Id) -> Self {
|
||||||
|
IdRef::new(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: for<'a> Deserialize<'a>, X: for<'a> Deserialize<'a>> From<&IdRef<X>> for IdRef<T> {
|
||||||
|
fn from(value: &IdRef<X>) -> Self {
|
||||||
|
IdRef::new(value.id.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Identifiable + for<'a> Deserialize<'a>> From<&Model<T>> for IdRef<T> {
|
||||||
|
fn from(value: &Model<T>) -> Self {
|
||||||
|
IdRef::new(value.full_id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Identifiable + for<'a> Deserialize<'a>> From<&T> for IdRef<T> {
|
||||||
|
fn from(value: &T) -> Self {
|
||||||
|
IdRef::new(value.full_id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de, T> Deserialize<'de> for IdRef<T> {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
Ok(IdRef {
|
||||||
|
id: s,
|
||||||
|
model: std::marker::PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Serialize for IdRef<T> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
self.id.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: for<'a> Deserialize<'a> + Send + Sync + Identifiable + Serialize + 'static> IdRef<T> {
|
||||||
|
pub fn fetch_raw(&self, db: &Database) -> Model<T> {
|
||||||
|
let id = self.id.clone();
|
||||||
|
db.get::<T, _>(id).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_fetch(&self, db: &Database) -> Option<Model<T>> {
|
||||||
|
// todo : asset type
|
||||||
|
|
||||||
|
Some(self.fetch_raw(db))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: for<'a> Deserialize<'a>> IdRef<T> {
|
||||||
|
pub fn new(id: String) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
model: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn fetch_raw_vfs(&self, vfs: &dyn vfs::FileSystem) -> T {
|
||||||
|
Store::get_id(&self.id, vfs).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn fetch_as<O: for<'a> Deserialize<'a>>(&self, vfs: &dyn vfs::FileSystem) -> O {
|
||||||
|
Store::get_id(&self.id, vfs).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Identifiable + Serialize + Send + Sync + 'static + for<'a> Deserialize<'a>> IdRef<T> {
|
||||||
|
/// Get the object this reference holds
|
||||||
|
pub unsafe fn get_raw(&self, vfs: &dyn vfs::FileSystem) -> Option<T> {
|
||||||
|
let split: Vec<&str> = self.id.split("::").collect();
|
||||||
|
if split.len() > 1 {
|
||||||
|
Store::get_id(&self.id, vfs)
|
||||||
|
} else {
|
||||||
|
Store::get(&T::model_id(), &self.id, vfs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dereference(&self, db: &Database) -> Model<T> {
|
||||||
|
self.try_dereference(db).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_dereference(&self, db: &Database) -> Option<Model<T>> {
|
||||||
|
db.get(self.id.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
T: Identifiable + Saveable + Send + Sync + 'static,
|
||||||
|
S: Identifiable + Saveable + Send + Sync + 'static,
|
||||||
|
R: RelationRef,
|
||||||
|
U,
|
||||||
|
> Relation<T, S, R, U>
|
||||||
|
{
|
||||||
|
pub unsafe fn top(reference: &Model<R>, db: &Database) -> Model<T> {
|
||||||
|
db.get(reference.read().top().id.clone()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn sub(reference: &Model<R>, db: &Database) -> Model<S> {
|
||||||
|
db.get(reference.read().sub().id.clone()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a relation ref id into the respective pointer structure.
|
||||||
|
///
|
||||||
|
/// The ref id looks like: `[top_id->sub_id]`
|
||||||
|
pub fn parse_ref_id(id: &str) -> (String, String) {
|
||||||
|
// Expected format: "[top_id->sub_id]"
|
||||||
|
if let Some(stripped) = id.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
|
||||||
|
if let Some((top, sub)) = stripped.split_once("->") {
|
||||||
|
return (top.to_string(), sub.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("Invalid ref_id format: {}", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a relation ref id for `top` and `sub`
|
||||||
|
pub fn ref_id(top: &Model<T>, sub: &Model<S>) -> String {
|
||||||
|
format!("[{}->{}]", top.reference(), sub.reference())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a collection id for this relation
|
||||||
|
pub fn collection_id(&self) -> String {
|
||||||
|
format!("{}-refs", self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
T: Identifiable + Saveable + Send + Sync + 'static,
|
||||||
|
S: Identifiable + Saveable + Send + Sync + 'static,
|
||||||
|
R: RelationRef,
|
||||||
|
> Relation<T, S, R, RelationKind::Unidirectional>
|
||||||
|
{
|
||||||
|
/// Get a `RelationReference` if a relation exists between `top` and `sub`
|
||||||
|
pub fn get(&self, top: &Model<T>, sub: &Model<S>, db: &Database) -> Option<Model<R>> {
|
||||||
|
let entry = db.get(Relation::<T, S, R, RelationKind::Unidirectional>::ref_id(
|
||||||
|
top, sub,
|
||||||
|
));
|
||||||
|
let entry_rev = db.get(Relation::<S, T, R, RelationKind::Unidirectional>::ref_id(
|
||||||
|
sub, top,
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some(entry) = entry {
|
||||||
|
return Some(entry);
|
||||||
|
}
|
||||||
|
if let Some(entry) = entry_rev {
|
||||||
|
return Some(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all `RelationReference`s with `node`
|
||||||
|
pub fn get_all_with_t<X: Into<IdRef<T>>>(
|
||||||
|
&self,
|
||||||
|
node: X,
|
||||||
|
vfs: &dyn vfs::FileSystem,
|
||||||
|
) -> Vec<IdRef<R>> {
|
||||||
|
let ids = Store::get_ids(&self.collection_id(), vfs);
|
||||||
|
let node: IdRef<T> = node.into();
|
||||||
|
let top_id = node.id;
|
||||||
|
ids.into_iter()
|
||||||
|
.filter_map(|x| {
|
||||||
|
let (t_id, s_id) =
|
||||||
|
Relation::<T, S, R, RelationKind::Unidirectional>::parse_ref_id(&x);
|
||||||
|
if top_id == t_id || top_id == s_id {
|
||||||
|
return Some(IdRef::new(x.clone()));
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all `RelationReference`s with `node`
|
||||||
|
pub fn get_all_with_s<X: Into<IdRef<S>>>(
|
||||||
|
&self,
|
||||||
|
node: X,
|
||||||
|
vfs: &dyn vfs::FileSystem,
|
||||||
|
) -> Vec<IdRef<R>> {
|
||||||
|
let ids = Store::get_ids(&self.collection_id(), vfs);
|
||||||
|
let node: IdRef<S> = node.into();
|
||||||
|
let top_id = node.id;
|
||||||
|
ids.into_iter()
|
||||||
|
.filter_map(|x| {
|
||||||
|
let (t_id, s_id) =
|
||||||
|
Relation::<T, S, R, RelationKind::Unidirectional>::parse_ref_id(&x);
|
||||||
|
if top_id == t_id || top_id == s_id {
|
||||||
|
return Some(IdRef::new(x.clone()));
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a relation exists between `top` and `sub`
|
||||||
|
pub fn exists<V: vfs::FileSystem>(&self, top: &Model<T>, sub: &Model<S>, vfs: &V) -> bool {
|
||||||
|
let entry: Option<RelationReference> = unsafe {
|
||||||
|
Store::get_opt(
|
||||||
|
&self.collection_id(),
|
||||||
|
&Relation::<T, S, R, RelationKind::Unidirectional>::ref_id(&top, &sub),
|
||||||
|
vfs,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let entry_rev: Option<RelationReference> = unsafe {
|
||||||
|
Store::get_opt(
|
||||||
|
&self.collection_id(),
|
||||||
|
&Relation::<S, T, R, RelationKind::Unidirectional>::ref_id(&sub, &top),
|
||||||
|
vfs,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if entry.is_some() || entry_rev.is_some() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a relation for `top` and `sub` with optional `weight` and `meta` values
|
||||||
|
pub fn add(
|
||||||
|
&self,
|
||||||
|
top: &Model<T>,
|
||||||
|
sub: &Model<S>,
|
||||||
|
weight: Option<f64>,
|
||||||
|
meta: Option<serde_json::Value>,
|
||||||
|
db: &Database,
|
||||||
|
) {
|
||||||
|
let mut reference = self.get(&top, &sub, db).unwrap_or_else(|| {
|
||||||
|
Model::from_raw(R::from_ref(RelationReference {
|
||||||
|
top: IdRef::new(top.full_id()),
|
||||||
|
sub: IdRef::new(sub.full_id()),
|
||||||
|
uni: true,
|
||||||
|
weight: weight,
|
||||||
|
meta: None,
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
reference.write(db, |r| {
|
||||||
|
r.update_meta(weight, meta.clone());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
T: Identifiable + Saveable + Send + Sync + 'static,
|
||||||
|
S: Identifiable + Saveable + Send + Sync + 'static,
|
||||||
|
R: RelationRef,
|
||||||
|
> Relation<T, S, R, RelationKind::Bidirectional>
|
||||||
|
{
|
||||||
|
/// Get a `RelationReference` if a relation exists between `top` and `sub`
|
||||||
|
pub unsafe fn get(&self, top: &Model<T>, sub: &Model<S>, db: &Database) -> Option<Model<R>> {
|
||||||
|
let entry = db.get(Relation::<T, S, R, RelationKind::Bidirectional>::ref_id(
|
||||||
|
top, sub,
|
||||||
|
));
|
||||||
|
if let Some(entry) = entry {
|
||||||
|
return Some(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all `RelationReference`s with `node` as top
|
||||||
|
pub fn get_all_top(&self, top: &Model<T>, vfs: &dyn vfs::FileSystem) -> Vec<IdRef<R>> {
|
||||||
|
let ids = Store::get_ids(&self.collection_id(), vfs);
|
||||||
|
let top_id = top.full_id();
|
||||||
|
ids.into_iter()
|
||||||
|
.filter_map(|x| {
|
||||||
|
let (t_id, _) = Relation::<T, S, R, RelationKind::Unidirectional>::parse_ref_id(&x);
|
||||||
|
if top_id == t_id {
|
||||||
|
return Some(IdRef::new(format!("{}::{x}", R::model_id())));
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all `RelationReference`s with `node` as sub
|
||||||
|
pub fn get_all_sub(&self, sub: &Model<T>, vfs: &dyn vfs::FileSystem) -> Vec<IdRef<R>> {
|
||||||
|
let ids = Store::get_ids(&self.collection_id(), vfs);
|
||||||
|
let sub_id = sub.full_id();
|
||||||
|
ids.into_iter()
|
||||||
|
.filter_map(|x| {
|
||||||
|
let (_, s_id) = Relation::<T, S, R, RelationKind::Unidirectional>::parse_ref_id(&x);
|
||||||
|
if sub_id == s_id {
|
||||||
|
return Some(IdRef::new(x));
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a relation exists between `top` and `sub`
|
||||||
|
pub fn exists(&self, top: &Model<T>, sub: &Model<S>, vfs: &dyn vfs::FileSystem) -> bool {
|
||||||
|
let entry: Option<RelationReference> = unsafe {
|
||||||
|
Store::get_opt(
|
||||||
|
&self.collection_id(),
|
||||||
|
&Relation::<T, S, R, RelationKind::Bidirectional>::ref_id(&top, &sub),
|
||||||
|
vfs,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if entry.is_some() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a relation for `top` and `sub` with optional `weight` and `meta` values
|
||||||
|
pub fn add(
|
||||||
|
&self,
|
||||||
|
top: &Model<T>,
|
||||||
|
sub: &Model<S>,
|
||||||
|
weight: Option<f64>,
|
||||||
|
meta: Option<serde_json::Value>,
|
||||||
|
db: &Database,
|
||||||
|
) {
|
||||||
|
let mut reference = unsafe {
|
||||||
|
self.get(&top, &sub, db).unwrap_or_else(|| {
|
||||||
|
Model::from_raw(R::from_ref(RelationReference {
|
||||||
|
top: IdRef::new(top.full_id()),
|
||||||
|
sub: IdRef::new(sub.full_id()),
|
||||||
|
uni: true,
|
||||||
|
weight: weight,
|
||||||
|
meta: None,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
reference.write(db, |reference: &mut _| {
|
||||||
|
reference.update_meta(weight, meta.clone());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RelationRef:
|
||||||
|
Identifiable + for<'a> Deserialize<'a> + Serialize + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
fn top(&self) -> IdRef<serde_json::Value>;
|
||||||
|
fn sub(&self) -> IdRef<serde_json::Value>;
|
||||||
|
fn from_ref(r: RelationReference) -> Self;
|
||||||
|
fn update_meta(&mut self, weight: Option<f64>, meta: Option<serde_json::Value>);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct RelationReference {
|
||||||
|
pub top: IdRef<serde_json::Value>,
|
||||||
|
pub sub: IdRef<serde_json::Value>,
|
||||||
|
pub uni: bool,
|
||||||
|
pub weight: Option<f64>,
|
||||||
|
pub meta: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RelationReference {
|
||||||
|
pub fn alter_meta<F: Fn(&mut X), X: for<'a> Deserialize<'a> + Default + Serialize>(
|
||||||
|
&mut self,
|
||||||
|
u: F,
|
||||||
|
) {
|
||||||
|
let mut meta =
|
||||||
|
serde_json::from_value(self.meta.clone().unwrap()).unwrap_or_else(|_| X::default());
|
||||||
|
u(&mut meta);
|
||||||
|
self.meta = Some(serde_json::to_value(meta).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> String {
|
||||||
|
format!("[{}->{}]", self.top.id, self.sub.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub unsafe fn save(&self, vfs: &dyn vfs::FileSystem) {
|
||||||
|
Store::save_raw(&self.model_id_real(), &self.id(), self, false, vfs);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dereference a `Vec<_>` with `IdRef<T>`s into their objects
|
||||||
|
pub fn dereference<
|
||||||
|
T: for<'a> Deserialize<'a> + Serialize + Identifiable + Send + Sync + 'static,
|
||||||
|
>(
|
||||||
|
v: &[IdRef<T>],
|
||||||
|
db: &Database,
|
||||||
|
) -> Vec<Model<T>> {
|
||||||
|
v.into_iter().map(|x| x.fetch_raw(db)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn dereference_raw<T: for<'a> Deserialize<'a>>(v: &[IdRef<T>], db: &Database) -> Vec<T> {
|
||||||
|
v.into_iter()
|
||||||
|
.map(|x| unsafe { x.fetch_raw_vfs(&*db.storage) })
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : generally clean up traversal loops
|
||||||
|
pub fn clean_graph_traversal<T>(id: &str, route: &[IdRef<T>]) -> Vec<IdRef<T>> {
|
||||||
|
let mut index = 0;
|
||||||
|
|
||||||
|
for (i, node) in route.iter().enumerate() {
|
||||||
|
let node = node.id.to_string();
|
||||||
|
if node == id {
|
||||||
|
index = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
route[index..].to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a path from `from` to `to` with max `reach` steps to get there.
|
||||||
|
///
|
||||||
|
/// `fetch_relation` is a function for traversing the graph ( `Fn(IdRef<T>, &Database) -> Vec<IdRef<T>>` )
|
||||||
|
pub fn find_path<F, T: Debug>(
|
||||||
|
from: IdRef<T>,
|
||||||
|
to: IdRef<T>,
|
||||||
|
reach: u32,
|
||||||
|
fetch_relation: F,
|
||||||
|
db: &Database,
|
||||||
|
) -> Vec<IdRef<T>>
|
||||||
|
where
|
||||||
|
F: Fn(&IdRef<T>, &Database) -> Vec<IdRef<T>> + Sync + Send + Clone,
|
||||||
|
{
|
||||||
|
log::info!("path from {from} r {reach}");
|
||||||
|
let my_friends = fetch_relation(&from, db);
|
||||||
|
log::trace!("have children {my_friends:?}");
|
||||||
|
|
||||||
|
if reach == 0 {
|
||||||
|
log::trace!("no reach :(");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
my_friends
|
||||||
|
.par_iter()
|
||||||
|
.filter_map(|friend| {
|
||||||
|
if friend.id == to.id {
|
||||||
|
log::trace!("found you {friend}");
|
||||||
|
return Some(vec![friend.clone()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!("nooo... looking deeper");
|
||||||
|
let p = find_path(
|
||||||
|
friend.clone(),
|
||||||
|
to.clone(),
|
||||||
|
reach - 1,
|
||||||
|
fetch_relation.clone(),
|
||||||
|
db,
|
||||||
|
);
|
||||||
|
if !p.is_empty() {
|
||||||
|
let mut ret = vec![friend.clone()];
|
||||||
|
ret.extend(p.iter().cloned());
|
||||||
|
return Some(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.find_any(|_| true) // find any successful path in parallel
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
log::trace!("END SEARCH");
|
||||||
|
vec![]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the other which isnt oneself from unidirectional `RelationReference`s
|
||||||
|
pub fn get_other<
|
||||||
|
X: Identifiable + Send + Sync + Serialize + for<'a> Deserialize<'a>,
|
||||||
|
T: Into<IdRef<X>>,
|
||||||
|
R: RelationRef,
|
||||||
|
>(
|
||||||
|
id: T,
|
||||||
|
refs: Vec<IdRef<R>>,
|
||||||
|
db: &Database,
|
||||||
|
) -> Vec<IdRef<serde_json::Value>> {
|
||||||
|
let mut other = Vec::new();
|
||||||
|
let id: IdRef<X> = id.into();
|
||||||
|
let id = id.id;
|
||||||
|
|
||||||
|
for r in refs {
|
||||||
|
if let Some(r) = r.try_dereference(&db) {
|
||||||
|
if r.read().top().id != id {
|
||||||
|
other.push(r.read().top());
|
||||||
|
} else {
|
||||||
|
other.push(r.read().sub());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::error!("Could not get Model<{}> from {}", R::model_id(), r.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
other
|
||||||
|
}
|
118
src/db/store.rs
Normal file
118
src/db/store.rs
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::Identifiable;
|
||||||
|
|
||||||
|
fn read_to_vec(path: &str, vfs: &dyn vfs::FileSystem) -> Option<Vec<u8>> {
|
||||||
|
let mut f = vfs.open_file(path).ok()?;
|
||||||
|
let mut content = Vec::new();
|
||||||
|
f.read_to_end(&mut content).ok()?;
|
||||||
|
Some(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(path: &str, data: &[u8], vfs: &dyn vfs::FileSystem) {
|
||||||
|
let mut f = vfs.create_file(path).unwrap();
|
||||||
|
f.write_all(&data).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Store {}
|
||||||
|
|
||||||
|
impl Store {
|
||||||
|
pub unsafe fn get<T: for<'a> Deserialize<'a>>(
|
||||||
|
collection: &str,
|
||||||
|
id: &str,
|
||||||
|
vfs: &dyn vfs::FileSystem,
|
||||||
|
) -> Option<T> {
|
||||||
|
let path = std::path::Path::new(&format!("/collection/{collection}"))
|
||||||
|
.join(id)
|
||||||
|
.display()
|
||||||
|
.to_string();
|
||||||
|
log::info!("IO READ {path}");
|
||||||
|
let content = read_to_vec(&path, vfs)?;
|
||||||
|
rmp_serde::from_slice(&content).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn get_opt<T: for<'a> Deserialize<'a>>(
|
||||||
|
collection: &str,
|
||||||
|
id: &str,
|
||||||
|
vfs: &dyn vfs::FileSystem,
|
||||||
|
) -> Option<T> {
|
||||||
|
let path = std::path::Path::new(&format!("/collection/{collection}"))
|
||||||
|
.join(id)
|
||||||
|
.display()
|
||||||
|
.to_string();
|
||||||
|
rmp_serde::from_slice(&read_to_vec(&path, vfs)?).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn save_raw<T: Serialize>(
|
||||||
|
collection: &str,
|
||||||
|
id: &str,
|
||||||
|
data: &T,
|
||||||
|
named: bool,
|
||||||
|
vfs: &dyn vfs::FileSystem,
|
||||||
|
) {
|
||||||
|
let c = if named {
|
||||||
|
rmp_serde::to_vec_named(data).unwrap()
|
||||||
|
} else {
|
||||||
|
rmp_serde::to_vec(data).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = std::path::Path::new(&format!("/collection/{collection}"))
|
||||||
|
.join(id)
|
||||||
|
.display()
|
||||||
|
.to_string();
|
||||||
|
let _ = vfs.create_dir(&format!("/collection/{collection}"));
|
||||||
|
log::info!("IO WRITE {path}");
|
||||||
|
write(&path, &c, vfs);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn save<T: Serialize + Identifiable>(
|
||||||
|
collection: &str,
|
||||||
|
data: &T,
|
||||||
|
named: bool,
|
||||||
|
vfs: &dyn vfs::FileSystem,
|
||||||
|
) {
|
||||||
|
Self::save_raw(collection, &data.id().to_string(), &data, named, vfs);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn get_id<T: for<'a> Deserialize<'a>>(
|
||||||
|
id: &str,
|
||||||
|
vfs: &dyn vfs::FileSystem,
|
||||||
|
) -> Option<T> {
|
||||||
|
let split_d: Vec<_> = id.split("[").collect();
|
||||||
|
let id = split_d.first()?;
|
||||||
|
let mut split: Vec<_> = id.split("::").collect();
|
||||||
|
let sid: &str = split.pop()?;
|
||||||
|
let collection = split.join("::");
|
||||||
|
let id = format!(
|
||||||
|
"{}{sid}{}",
|
||||||
|
if split_d.len() > 1 { "[" } else { "" },
|
||||||
|
split_d
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("[")
|
||||||
|
);
|
||||||
|
Store::get(&collection, &id, vfs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_ids(collection: &str, vfs: &dyn vfs::FileSystem) -> Vec<String> {
|
||||||
|
vfs.read_dir(&format!("/collection/{collection}"))
|
||||||
|
.unwrap()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(vfs: &dyn vfs::FileSystem) -> Vec<String> {
|
||||||
|
vfs.read_dir("/collection").unwrap().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Saveable: Identifiable + for<'a> Deserialize<'a> + Serialize {
|
||||||
|
unsafe fn save(&self, named: bool, vfs: &dyn vfs::FileSystem) {
|
||||||
|
Store::save(&Self::model_id(), self, named, vfs);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn get(id: &str, vfs: &dyn vfs::FileSystem) -> Option<Self> {
|
||||||
|
Store::get(&Self::model_id(), id, vfs)
|
||||||
|
}
|
||||||
|
}
|
110
src/lib.rs
Normal file
110
src/lib.rs
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
//mod model;
|
||||||
|
|
||||||
|
use db::id::Id;
|
||||||
|
|
||||||
|
pub mod db;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
pub use owl_macro::model;
|
||||||
|
pub use owl_macro::relation;
|
||||||
|
pub use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub static DB: OnceCell<db::Database> = OnceCell::new();
|
||||||
|
|
||||||
|
/// Set a `Database` as global.
|
||||||
|
///
|
||||||
|
/// All global macro actions are then done against that `Database`.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! set_global_db {
|
||||||
|
($db:ident) => {
|
||||||
|
assert!($crate::DB.set($db).is_ok());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! dereference {
|
||||||
|
($id:ident) => {
|
||||||
|
$id.dereference(&$crate::DB.get().unwrap())
|
||||||
|
};
|
||||||
|
($id:expr) => {
|
||||||
|
$id.dereference(&$crate::DB.get().unwrap())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! save {
|
||||||
|
($model:ident) => {
|
||||||
|
$crate::DB.get().unwrap().save($model)
|
||||||
|
};
|
||||||
|
($model:expr) => {
|
||||||
|
$crate::DB.get().unwrap().save($model)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! get {
|
||||||
|
($id:ident) => {
|
||||||
|
$crate::DB.get().unwrap().get($id)
|
||||||
|
};
|
||||||
|
($id:expr) => {
|
||||||
|
$crate::DB.get().unwrap().get($id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! update {
|
||||||
|
($ids:ident, $u:expr) => {
|
||||||
|
$crate::DB.get().unwrap().update($ids, $u)
|
||||||
|
};
|
||||||
|
($ids:expr, $u:expr) => {
|
||||||
|
$crate::DB.get().unwrap().update($ids, $u)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! query {
|
||||||
|
($id:ident) => {
|
||||||
|
$crate::DB.get().unwrap().query($id)
|
||||||
|
};
|
||||||
|
($id:expr) => {
|
||||||
|
$crate::DB.get().unwrap().query($id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod prelude {
|
||||||
|
pub use super::db::field::Historic;
|
||||||
|
pub use super::db::id::Id;
|
||||||
|
pub use super::db::relation::{dereference, IdRef, Relation, RelationKind};
|
||||||
|
pub use super::db::store::{Saveable, Store};
|
||||||
|
pub use super::db::Database;
|
||||||
|
pub use super::db::Model;
|
||||||
|
pub use super::Identifiable;
|
||||||
|
pub use super::{model, relation};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO : wire format for endpoints
|
||||||
|
// TODO : auth + traceability
|
||||||
|
// TODO : deletion?
|
||||||
|
// TODO : make relations able to be inactive/active
|
||||||
|
|
||||||
|
pub trait Identifiable: Sized + for<'a> Deserialize<'a> {
|
||||||
|
fn id(&self) -> Id;
|
||||||
|
|
||||||
|
/// Get a typed `IdRef<T>` for this ID
|
||||||
|
fn reference(&self) -> crate::db::relation::IdRef<Self> {
|
||||||
|
crate::db::relation::IdRef::new(self.full_id())
|
||||||
|
}
|
||||||
|
fn model_id() -> String;
|
||||||
|
fn full_id(&self) -> String {
|
||||||
|
format!("{}::{}", Self::model_id(), self.id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Identifiable for serde_json::Value {
|
||||||
|
fn id(&self) -> Id {
|
||||||
|
serde_json::from_value(self.as_object().unwrap().get("id").unwrap().clone()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn model_id() -> String {
|
||||||
|
"json".to_string()
|
||||||
|
}
|
||||||
|
}
|
128
src/main.rs
Normal file
128
src/main.rs
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use owl::prelude::*;
|
||||||
|
|
||||||
|
// data points
|
||||||
|
/*
|
||||||
|
|
||||||
|
// events
|
||||||
|
struct Event {
|
||||||
|
pub ts: String,
|
||||||
|
pub content: String,
|
||||||
|
pub references: Vec<Reference>,
|
||||||
|
pub attachments: Vec<File>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct File(String);
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO : document
|
||||||
|
|
||||||
|
// TODO : serve api
|
||||||
|
// TODO : schemaless data storage (named)
|
||||||
|
// TODO : export/import?
|
||||||
|
|
||||||
|
// pub static PG: OnceCell<sqlx::PgPool> = OnceCell::const_new();
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! get_pg {
|
||||||
|
() => {
|
||||||
|
if let Some(client) = $crate::PG.get() {
|
||||||
|
client
|
||||||
|
} else {
|
||||||
|
let client = sqlx::postgres::PgPoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect(&std::env::var("DATABASE_URL").unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
$crate::PG.set(client).unwrap();
|
||||||
|
$crate::PG.get().unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
#[rocket::main]
|
||||||
|
async fn main() {
|
||||||
|
let cors = rocket_cors::CorsOptions {
|
||||||
|
allowed_origins: rocket_cors::AllowedOrigins::all(),
|
||||||
|
allowed_methods: vec![Method::Get, Method::Post, Method::Options]
|
||||||
|
.into_iter()
|
||||||
|
.map(From::from)
|
||||||
|
.collect(),
|
||||||
|
allowed_headers: rocket_cors::AllowedHeaders::all(),
|
||||||
|
allow_credentials: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.to_cors()
|
||||||
|
.expect("error creating CORS options");
|
||||||
|
|
||||||
|
rocket::build()
|
||||||
|
.mount(
|
||||||
|
"/",
|
||||||
|
route![
|
||||||
|
routes::model_api_create,
|
||||||
|
routes::model_api_info,
|
||||||
|
routes::model_api_update,
|
||||||
|
routes::model_overview
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.attach(cors)
|
||||||
|
//.manage(pg)
|
||||||
|
.launch()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
mod cli;
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
let cli: cli::OwlCLI = argh::from_env();
|
||||||
|
|
||||||
|
match cli.nested {
|
||||||
|
cli::OwlCLICommands::List(list_command) => {
|
||||||
|
let db_path = list_command.db.unwrap_or("./db".to_string());
|
||||||
|
let db = Database::filesystem(&db_path);
|
||||||
|
if let Some(collection) = list_command.collection {
|
||||||
|
let entries = db.list_entries(&collection);
|
||||||
|
println!("List {db_path} [{collection}]:");
|
||||||
|
for t in entries {
|
||||||
|
println!(" - {t}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let collections = db.list();
|
||||||
|
println!("List {db_path}:");
|
||||||
|
for c in collections {
|
||||||
|
println!(" - {c}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cli::OwlCLICommands::Get(get_command) => {
|
||||||
|
let db_path = get_command.db.unwrap_or("./db".to_string());
|
||||||
|
let db = Database::filesystem(&db_path);
|
||||||
|
|
||||||
|
let x: rmpv::Value = db.get_id(&get_command.collection, &get_command.id);
|
||||||
|
println!("{}", serde_json::to_string(&x).unwrap());
|
||||||
|
}
|
||||||
|
cli::OwlCLICommands::Store(store_command) => {
|
||||||
|
let db_path = store_command.db.unwrap_or("./db".to_string());
|
||||||
|
let db = Database::filesystem(&db_path).named();
|
||||||
|
|
||||||
|
let mut buffer = String::new();
|
||||||
|
std::io::stdin()
|
||||||
|
.read_to_string(&mut buffer)
|
||||||
|
.expect("Failed to read from stdin");
|
||||||
|
let json_value: serde_json::Value =
|
||||||
|
serde_json::from_str(&buffer).expect("Failed to parse JSON");
|
||||||
|
|
||||||
|
let id = json_value.id();
|
||||||
|
unsafe {
|
||||||
|
db.save_raw(&json_value, &store_command.collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}::{id}", store_command.collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
tests/basic.rs
Normal file
24
tests/basic.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use owl::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[model]
|
||||||
|
pub struct TestModel {
|
||||||
|
pub id: Id,
|
||||||
|
pub data: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_load_db() {
|
||||||
|
let data = "TestData".to_string();
|
||||||
|
let db = Database::in_memory();
|
||||||
|
|
||||||
|
let m = TestModel {
|
||||||
|
id: Id::String(data.clone()),
|
||||||
|
data: data.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
db.save(m);
|
||||||
|
|
||||||
|
let get_model: Model<TestModel> = db.get(data.as_str()).unwrap();
|
||||||
|
assert_eq!(get_model.read().id.to_string(), data);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue