mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
rename frontend to web and update contributing guide (#2509)
* rename frontend to web and update contributing guide * rename this time * fix build * restructure rust code * update documentation * update descriptions * Update CONTRIBUTING.md Co-authored-by: J H <2364004+Blu-J@users.noreply.github.com> --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: J H <2364004+Blu-J@users.noreply.github.com>
This commit is contained in:
370
core/startos/src/db/mod.rs
Normal file
370
core/startos/src/db/mod.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
pub mod model;
|
||||
pub mod package;
|
||||
pub mod prelude;
|
||||
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::{FutureExt, SinkExt, StreamExt};
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
use patch_db::{Dump, Revision};
|
||||
use rpc_toolkit::command;
|
||||
use rpc_toolkit::hyper::upgrade::Upgraded;
|
||||
use rpc_toolkit::hyper::{Body, Error as HyperError, Request, Response};
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::task::JoinError;
|
||||
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
|
||||
use tokio_tungstenite::tungstenite::protocol::CloseFrame;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::middleware::auth::{HasValidSession, HashSessionToken};
|
||||
use crate::prelude::*;
|
||||
use crate::util::display_none;
|
||||
use crate::util::serde::{display_serializable, IoFormat};
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn ws_handler<
|
||||
WSFut: Future<Output = Result<Result<WebSocketStream<Upgraded>, HyperError>, JoinError>>,
|
||||
>(
|
||||
ctx: RpcContext,
|
||||
session: Option<(HasValidSession, HashSessionToken)>,
|
||||
ws_fut: WSFut,
|
||||
) -> Result<(), Error> {
|
||||
let (dump, sub) = ctx.db.dump_and_sub().await;
|
||||
let mut stream = ws_fut
|
||||
.await
|
||||
.with_kind(ErrorKind::Network)?
|
||||
.with_kind(ErrorKind::Unknown)?;
|
||||
|
||||
if let Some((session, token)) = session {
|
||||
let kill = subscribe_to_session_kill(&ctx, token).await;
|
||||
send_dump(session, &mut stream, dump).await?;
|
||||
|
||||
deal_with_messages(session, kill, sub, stream).await?;
|
||||
} else {
|
||||
stream
|
||||
.close(Some(CloseFrame {
|
||||
code: CloseCode::Error,
|
||||
reason: "UNAUTHORIZED".into(),
|
||||
}))
|
||||
.await
|
||||
.with_kind(ErrorKind::Network)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn subscribe_to_session_kill(
|
||||
ctx: &RpcContext,
|
||||
token: HashSessionToken,
|
||||
) -> oneshot::Receiver<()> {
|
||||
let (send, recv) = oneshot::channel();
|
||||
let mut guard = ctx.open_authed_websockets.lock().await;
|
||||
if !guard.contains_key(&token) {
|
||||
guard.insert(token, vec![send]);
|
||||
} else {
|
||||
guard.get_mut(&token).unwrap().push(send);
|
||||
}
|
||||
recv
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn deal_with_messages(
|
||||
_has_valid_authentication: HasValidSession,
|
||||
mut kill: oneshot::Receiver<()>,
|
||||
mut sub: patch_db::Subscriber,
|
||||
mut stream: WebSocketStream<Upgraded>,
|
||||
) -> Result<(), Error> {
|
||||
let mut timer = tokio::time::interval(tokio::time::Duration::from_secs(5));
|
||||
|
||||
loop {
|
||||
futures::select! {
|
||||
_ = (&mut kill).fuse() => {
|
||||
tracing::info!("Closing WebSocket: Reason: Session Terminated");
|
||||
stream
|
||||
.close(Some(CloseFrame {
|
||||
code: CloseCode::Error,
|
||||
reason: "UNAUTHORIZED".into(),
|
||||
}))
|
||||
.await
|
||||
.with_kind(ErrorKind::Network)?;
|
||||
return Ok(())
|
||||
}
|
||||
new_rev = sub.recv().fuse() => {
|
||||
let rev = new_rev.expect("UNREACHABLE: patch-db is dropped");
|
||||
stream
|
||||
.send(Message::Text(serde_json::to_string(&rev).with_kind(ErrorKind::Serialization)?))
|
||||
.await
|
||||
.with_kind(ErrorKind::Network)?;
|
||||
}
|
||||
message = stream.next().fuse() => {
|
||||
let message = message.transpose().with_kind(ErrorKind::Network)?;
|
||||
match message {
|
||||
None => {
|
||||
tracing::info!("Closing WebSocket: Stream Finished");
|
||||
return Ok(())
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
// This is trying to give a health checks to the home to keep the ui alive.
|
||||
_ = timer.tick().fuse() => {
|
||||
stream
|
||||
.send(Message::Ping(vec![]))
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_dump(
|
||||
_has_valid_authentication: HasValidSession,
|
||||
stream: &mut WebSocketStream<Upgraded>,
|
||||
dump: Dump,
|
||||
) -> Result<(), Error> {
|
||||
stream
|
||||
.send(Message::Text(
|
||||
serde_json::to_string(&dump).with_kind(ErrorKind::Serialization)?,
|
||||
))
|
||||
.await
|
||||
.with_kind(ErrorKind::Network)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe(ctx: RpcContext, req: Request<Body>) -> Result<Response<Body>, Error> {
|
||||
let (parts, body) = req.into_parts();
|
||||
let session = match async {
|
||||
let token = HashSessionToken::from_request_parts(&parts)?;
|
||||
let session = HasValidSession::from_request_parts(&parts, &ctx).await?;
|
||||
Ok::<_, Error>((session, token))
|
||||
}
|
||||
.await
|
||||
{
|
||||
Ok(a) => Some(a),
|
||||
Err(e) => {
|
||||
if e.kind != ErrorKind::Authorization {
|
||||
tracing::error!("Error Authenticating Websocket: {}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
}
|
||||
None
|
||||
}
|
||||
};
|
||||
let req = Request::from_parts(parts, body);
|
||||
let (res, ws_fut) = hyper_ws_listener::create_ws(req).with_kind(ErrorKind::Network)?;
|
||||
if let Some(ws_fut) = ws_fut {
|
||||
tokio::task::spawn(async move {
|
||||
match ws_handler(ctx, session, ws_fut).await {
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
tracing::error!("WebSocket Closed: {}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[command(subcommands(dump, put, apply))]
|
||||
pub fn db() -> Result<(), RpcError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum RevisionsRes {
|
||||
Revisions(Vec<Arc<Revision>>),
|
||||
Dump(Dump),
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn cli_dump(
|
||||
ctx: CliContext,
|
||||
_format: Option<IoFormat>,
|
||||
path: Option<PathBuf>,
|
||||
) -> Result<Dump, RpcError> {
|
||||
let dump = if let Some(path) = path {
|
||||
PatchDb::open(path).await?.dump().await
|
||||
} else {
|
||||
rpc_toolkit::command_helpers::call_remote(
|
||||
ctx,
|
||||
"db.dump",
|
||||
serde_json::json!({}),
|
||||
std::marker::PhantomData::<Dump>,
|
||||
)
|
||||
.await?
|
||||
.result?
|
||||
};
|
||||
|
||||
Ok(dump)
|
||||
}
|
||||
|
||||
#[command(
|
||||
custom_cli(cli_dump(async, context(CliContext))),
|
||||
display(display_serializable)
|
||||
)]
|
||||
pub async fn dump(
|
||||
#[context] ctx: RpcContext,
|
||||
#[allow(unused_variables)]
|
||||
#[arg(long = "format")]
|
||||
format: Option<IoFormat>,
|
||||
#[allow(unused_variables)]
|
||||
#[arg]
|
||||
path: Option<PathBuf>,
|
||||
) -> Result<Dump, Error> {
|
||||
Ok(ctx.db.dump().await)
|
||||
}
|
||||
|
||||
fn apply_expr(input: jaq_core::Val, expr: &str) -> Result<jaq_core::Val, Error> {
|
||||
let (expr, errs) = jaq_core::parse::parse(expr, jaq_core::parse::main());
|
||||
|
||||
let Some(expr) = expr else {
|
||||
return Err(Error::new(
|
||||
eyre!("Failed to parse expression: {:?}", errs),
|
||||
crate::ErrorKind::InvalidRequest,
|
||||
));
|
||||
};
|
||||
|
||||
let mut errs = Vec::new();
|
||||
|
||||
let mut defs = jaq_core::Definitions::core();
|
||||
for def in jaq_std::std() {
|
||||
defs.insert(def, &mut errs);
|
||||
}
|
||||
|
||||
let filter = defs.finish(expr, Vec::new(), &mut errs);
|
||||
|
||||
if !errs.is_empty() {
|
||||
return Err(Error::new(
|
||||
eyre!("Failed to compile expression: {:?}", errs),
|
||||
crate::ErrorKind::InvalidRequest,
|
||||
));
|
||||
};
|
||||
|
||||
let inputs = jaq_core::RcIter::new(std::iter::empty());
|
||||
let mut res_iter = filter.run(jaq_core::Ctx::new([], &inputs), input);
|
||||
|
||||
let Some(res) = res_iter
|
||||
.next()
|
||||
.transpose()
|
||||
.map_err(|e| eyre!("{e}"))
|
||||
.with_kind(crate::ErrorKind::Deserialization)?
|
||||
else {
|
||||
return Err(Error::new(
|
||||
eyre!("expr returned no results"),
|
||||
crate::ErrorKind::InvalidRequest,
|
||||
));
|
||||
};
|
||||
|
||||
if res_iter.next().is_some() {
|
||||
return Err(Error::new(
|
||||
eyre!("expr returned too many results"),
|
||||
crate::ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn cli_apply(ctx: CliContext, expr: String, path: Option<PathBuf>) -> Result<(), RpcError> {
|
||||
if let Some(path) = path {
|
||||
PatchDb::open(path)
|
||||
.await?
|
||||
.mutate(|db| {
|
||||
let res = apply_expr(
|
||||
serde_json::to_value(patch_db::Value::from(db.clone()))
|
||||
.with_kind(ErrorKind::Deserialization)?
|
||||
.into(),
|
||||
&expr,
|
||||
)?;
|
||||
|
||||
db.ser(
|
||||
&serde_json::from_value::<model::Database>(res.clone().into()).with_ctx(
|
||||
|_| {
|
||||
(
|
||||
crate::ErrorKind::Deserialization,
|
||||
"result does not match database model",
|
||||
)
|
||||
},
|
||||
)?,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
rpc_toolkit::command_helpers::call_remote(
|
||||
ctx,
|
||||
"db.apply",
|
||||
serde_json::json!({ "expr": expr }),
|
||||
std::marker::PhantomData::<()>,
|
||||
)
|
||||
.await?
|
||||
.result?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(
|
||||
custom_cli(cli_apply(async, context(CliContext))),
|
||||
display(display_none)
|
||||
)]
|
||||
pub async fn apply(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] expr: String,
|
||||
#[allow(unused_variables)]
|
||||
#[arg]
|
||||
path: Option<PathBuf>,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
let res = apply_expr(
|
||||
serde_json::to_value(patch_db::Value::from(db.clone()))
|
||||
.with_kind(ErrorKind::Deserialization)?
|
||||
.into(),
|
||||
&expr,
|
||||
)?;
|
||||
|
||||
db.ser(
|
||||
&serde_json::from_value::<model::Database>(res.clone().into()).with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Deserialization,
|
||||
"result does not match database model",
|
||||
)
|
||||
})?,
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[command(subcommands(ui))]
|
||||
pub fn put() -> Result<(), RpcError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(display(display_serializable))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn ui(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] pointer: JsonPointer,
|
||||
#[arg] value: Value,
|
||||
#[allow(unused_variables)]
|
||||
#[arg(long = "format")]
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<(), Error> {
|
||||
let ptr = "/ui"
|
||||
.parse::<JsonPointer>()
|
||||
.with_kind(ErrorKind::Database)?
|
||||
+ &pointer;
|
||||
ctx.db.put(&ptr, &value).await?;
|
||||
Ok(())
|
||||
}
|
||||
527
core/startos/src/db/model.rs
Normal file
527
core/startos/src/db/model.rs
Normal file
@@ -0,0 +1,527 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use emver::VersionRange;
|
||||
use imbl_value::InternedString;
|
||||
use ipnet::{Ipv4Net, Ipv6Net};
|
||||
use isocountry::CountryCode;
|
||||
use itertools::Itertools;
|
||||
use models::{DataUrl, HealthCheckId, InterfaceId};
|
||||
use openssl::hash::MessageDigest;
|
||||
use patch_db::{HasModel, Value};
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssh_key::public::Ed25519PublicKey;
|
||||
|
||||
use crate::account::AccountInfo;
|
||||
use crate::config::spec::PackagePointerSpec;
|
||||
use crate::install::progress::InstallProgress;
|
||||
use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr};
|
||||
use crate::prelude::*;
|
||||
use crate::s9pk::manifest::{Manifest, PackageId};
|
||||
use crate::status::Status;
|
||||
use crate::util::Version;
|
||||
use crate::version::{Current, VersionT};
|
||||
use crate::{ARCH, PLATFORM};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
// #[macro_debug]
|
||||
pub struct Database {
|
||||
pub server_info: ServerInfo,
|
||||
pub package_data: AllPackageData,
|
||||
pub ui: Value,
|
||||
}
|
||||
impl Database {
|
||||
pub fn init(account: &AccountInfo) -> Self {
|
||||
let lan_address = account.hostname.lan_address().parse().unwrap();
|
||||
Database {
|
||||
server_info: ServerInfo {
|
||||
arch: get_arch(),
|
||||
platform: get_platform(),
|
||||
id: account.server_id.clone(),
|
||||
version: Current::new().semver().into(),
|
||||
hostname: account.hostname.no_dot_host_name(),
|
||||
last_backup: None,
|
||||
last_wifi_region: None,
|
||||
eos_version_compat: Current::new().compat().clone(),
|
||||
lan_address,
|
||||
tor_address: format!("https://{}", account.key.tor_address())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
ip_info: BTreeMap::new(),
|
||||
status_info: ServerStatus {
|
||||
backup_progress: None,
|
||||
updated: false,
|
||||
update_progress: None,
|
||||
shutting_down: false,
|
||||
restarting: false,
|
||||
},
|
||||
wifi: WifiInfo {
|
||||
ssids: Vec::new(),
|
||||
connected: None,
|
||||
selected: None,
|
||||
},
|
||||
unread_notification_count: 0,
|
||||
connection_addresses: ConnectionAddresses {
|
||||
tor: Vec::new(),
|
||||
clearnet: Vec::new(),
|
||||
},
|
||||
password_hash: account.password.clone(),
|
||||
pubkey: ssh_key::PublicKey::from(Ed25519PublicKey::from(&account.key.ssh_key()))
|
||||
.to_openssh()
|
||||
.unwrap(),
|
||||
ca_fingerprint: account
|
||||
.root_ca_cert
|
||||
.digest(MessageDigest::sha256())
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|x| format!("{x:X}"))
|
||||
.join(":"),
|
||||
ntp_synced: false,
|
||||
zram: true,
|
||||
},
|
||||
package_data: AllPackageData::default(),
|
||||
ui: serde_json::from_str(include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../web/patchdb-ui-seed.json"
|
||||
)))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type DatabaseModel = Model<Database>;
|
||||
|
||||
fn get_arch() -> InternedString {
|
||||
(*ARCH).into()
|
||||
}
|
||||
|
||||
fn get_platform() -> InternedString {
|
||||
(&*PLATFORM).into()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct ServerInfo {
|
||||
#[serde(default = "get_arch")]
|
||||
pub arch: InternedString,
|
||||
#[serde(default = "get_platform")]
|
||||
pub platform: InternedString,
|
||||
pub id: String,
|
||||
pub hostname: String,
|
||||
pub version: Version,
|
||||
pub last_backup: Option<DateTime<Utc>>,
|
||||
/// Used in the wifi to determine the region to set the system to
|
||||
pub last_wifi_region: Option<CountryCode>,
|
||||
pub eos_version_compat: VersionRange,
|
||||
pub lan_address: Url,
|
||||
pub tor_address: Url,
|
||||
pub ip_info: BTreeMap<String, IpInfo>,
|
||||
#[serde(default)]
|
||||
pub status_info: ServerStatus,
|
||||
pub wifi: WifiInfo,
|
||||
pub unread_notification_count: u64,
|
||||
pub connection_addresses: ConnectionAddresses,
|
||||
pub password_hash: String,
|
||||
pub pubkey: String,
|
||||
pub ca_fingerprint: String,
|
||||
#[serde(default)]
|
||||
pub ntp_synced: bool,
|
||||
#[serde(default)]
|
||||
pub zram: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct IpInfo {
|
||||
pub ipv4_range: Option<Ipv4Net>,
|
||||
pub ipv4: Option<Ipv4Addr>,
|
||||
pub ipv6_range: Option<Ipv6Net>,
|
||||
pub ipv6: Option<Ipv6Addr>,
|
||||
}
|
||||
impl IpInfo {
|
||||
pub async fn for_interface(iface: &str) -> Result<Self, Error> {
|
||||
let (ipv4, ipv4_range) = get_iface_ipv4_addr(iface).await?.unzip();
|
||||
let (ipv6, ipv6_range) = get_iface_ipv6_addr(iface).await?.unzip();
|
||||
Ok(Self {
|
||||
ipv4_range,
|
||||
ipv4,
|
||||
ipv6_range,
|
||||
ipv6,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel)]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct BackupProgress {
|
||||
pub complete: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct ServerStatus {
|
||||
pub backup_progress: Option<BTreeMap<PackageId, BackupProgress>>,
|
||||
pub updated: bool,
|
||||
pub update_progress: Option<UpdateProgress>,
|
||||
#[serde(default)]
|
||||
pub shutting_down: bool,
|
||||
#[serde(default)]
|
||||
pub restarting: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct UpdateProgress {
|
||||
pub size: Option<u64>,
|
||||
pub downloaded: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct WifiInfo {
|
||||
pub ssids: Vec<String>,
|
||||
pub selected: Option<String>,
|
||||
pub connected: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ServerSpecs {
|
||||
pub cpu: String,
|
||||
pub disk: String,
|
||||
pub memory: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ConnectionAddresses {
|
||||
pub tor: Vec<String>,
|
||||
pub clearnet: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct AllPackageData(pub BTreeMap<PackageId, PackageDataEntry>);
|
||||
impl Map for AllPackageData {
|
||||
type Key = PackageId;
|
||||
type Value = PackageDataEntry;
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct StaticFiles {
|
||||
license: String,
|
||||
instructions: String,
|
||||
icon: String,
|
||||
}
|
||||
impl StaticFiles {
|
||||
pub fn local(id: &PackageId, version: &Version, icon_type: &str) -> Self {
|
||||
StaticFiles {
|
||||
license: format!("/public/package-data/{}/{}/LICENSE.md", id, version),
|
||||
instructions: format!("/public/package-data/{}/{}/INSTRUCTIONS.md", id, version),
|
||||
icon: format!("/public/package-data/{}/{}/icon.{}", id, version, icon_type),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct PackageDataEntryInstalling {
|
||||
pub static_files: StaticFiles,
|
||||
pub manifest: Manifest,
|
||||
pub install_progress: Arc<InstallProgress>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct PackageDataEntryUpdating {
|
||||
pub static_files: StaticFiles,
|
||||
pub manifest: Manifest,
|
||||
pub installed: InstalledPackageInfo,
|
||||
pub install_progress: Arc<InstallProgress>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct PackageDataEntryRestoring {
|
||||
pub static_files: StaticFiles,
|
||||
pub manifest: Manifest,
|
||||
pub install_progress: Arc<InstallProgress>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct PackageDataEntryRemoving {
|
||||
pub static_files: StaticFiles,
|
||||
pub manifest: Manifest,
|
||||
pub removing: InstalledPackageInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct PackageDataEntryInstalled {
|
||||
pub static_files: StaticFiles,
|
||||
pub manifest: Manifest,
|
||||
pub installed: InstalledPackageInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(tag = "state")]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
// #[macro_debug]
|
||||
pub enum PackageDataEntry {
|
||||
Installing(PackageDataEntryInstalling),
|
||||
Updating(PackageDataEntryUpdating),
|
||||
Restoring(PackageDataEntryRestoring),
|
||||
Removing(PackageDataEntryRemoving),
|
||||
Installed(PackageDataEntryInstalled),
|
||||
}
|
||||
impl Model<PackageDataEntry> {
|
||||
pub fn expect_into_installed(self) -> Result<Model<PackageDataEntryInstalled>, Error> {
|
||||
if let PackageDataEntryMatchModel::Installed(a) = self.into_match() {
|
||||
Ok(a)
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("package is not in installed state"),
|
||||
ErrorKind::InvalidRequest,
|
||||
))
|
||||
}
|
||||
}
|
||||
pub fn expect_as_installed(&self) -> Result<&Model<PackageDataEntryInstalled>, Error> {
|
||||
if let PackageDataEntryMatchModelRef::Installed(a) = self.as_match() {
|
||||
Ok(a)
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("package is not in installed state"),
|
||||
ErrorKind::InvalidRequest,
|
||||
))
|
||||
}
|
||||
}
|
||||
pub fn expect_as_installed_mut(
|
||||
&mut self,
|
||||
) -> Result<&mut Model<PackageDataEntryInstalled>, Error> {
|
||||
if let PackageDataEntryMatchModelMut::Installed(a) = self.as_match_mut() {
|
||||
Ok(a)
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("package is not in installed state"),
|
||||
ErrorKind::InvalidRequest,
|
||||
))
|
||||
}
|
||||
}
|
||||
pub fn expect_into_removing(self) -> Result<Model<PackageDataEntryRemoving>, Error> {
|
||||
if let PackageDataEntryMatchModel::Removing(a) = self.into_match() {
|
||||
Ok(a)
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("package is not in removing state"),
|
||||
ErrorKind::InvalidRequest,
|
||||
))
|
||||
}
|
||||
}
|
||||
pub fn expect_as_removing(&self) -> Result<&Model<PackageDataEntryRemoving>, Error> {
|
||||
if let PackageDataEntryMatchModelRef::Removing(a) = self.as_match() {
|
||||
Ok(a)
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("package is not in removing state"),
|
||||
ErrorKind::InvalidRequest,
|
||||
))
|
||||
}
|
||||
}
|
||||
pub fn expect_as_removing_mut(
|
||||
&mut self,
|
||||
) -> Result<&mut Model<PackageDataEntryRemoving>, Error> {
|
||||
if let PackageDataEntryMatchModelMut::Removing(a) = self.as_match_mut() {
|
||||
Ok(a)
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("package is not in removing state"),
|
||||
ErrorKind::InvalidRequest,
|
||||
))
|
||||
}
|
||||
}
|
||||
pub fn expect_as_installing_mut(
|
||||
&mut self,
|
||||
) -> Result<&mut Model<PackageDataEntryInstalling>, Error> {
|
||||
if let PackageDataEntryMatchModelMut::Installing(a) = self.as_match_mut() {
|
||||
Ok(a)
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("package is not in installing state"),
|
||||
ErrorKind::InvalidRequest,
|
||||
))
|
||||
}
|
||||
}
|
||||
pub fn into_manifest(self) -> Model<Manifest> {
|
||||
match self.into_match() {
|
||||
PackageDataEntryMatchModel::Installing(a) => a.into_manifest(),
|
||||
PackageDataEntryMatchModel::Updating(a) => a.into_installed().into_manifest(),
|
||||
PackageDataEntryMatchModel::Restoring(a) => a.into_manifest(),
|
||||
PackageDataEntryMatchModel::Removing(a) => a.into_manifest(),
|
||||
PackageDataEntryMatchModel::Installed(a) => a.into_manifest(),
|
||||
PackageDataEntryMatchModel::Error(_) => Model::from(Value::Null),
|
||||
}
|
||||
}
|
||||
pub fn as_manifest(&self) -> &Model<Manifest> {
|
||||
match self.as_match() {
|
||||
PackageDataEntryMatchModelRef::Installing(a) => a.as_manifest(),
|
||||
PackageDataEntryMatchModelRef::Updating(a) => a.as_installed().as_manifest(),
|
||||
PackageDataEntryMatchModelRef::Restoring(a) => a.as_manifest(),
|
||||
PackageDataEntryMatchModelRef::Removing(a) => a.as_manifest(),
|
||||
PackageDataEntryMatchModelRef::Installed(a) => a.as_manifest(),
|
||||
PackageDataEntryMatchModelRef::Error(_) => (&Value::Null).into(),
|
||||
}
|
||||
}
|
||||
pub fn into_installed(self) -> Option<Model<InstalledPackageInfo>> {
|
||||
match self.into_match() {
|
||||
PackageDataEntryMatchModel::Installing(_) => None,
|
||||
PackageDataEntryMatchModel::Updating(a) => Some(a.into_installed()),
|
||||
PackageDataEntryMatchModel::Restoring(_) => None,
|
||||
PackageDataEntryMatchModel::Removing(_) => None,
|
||||
PackageDataEntryMatchModel::Installed(a) => Some(a.into_installed()),
|
||||
PackageDataEntryMatchModel::Error(_) => None,
|
||||
}
|
||||
}
|
||||
pub fn as_installed(&self) -> Option<&Model<InstalledPackageInfo>> {
|
||||
match self.as_match() {
|
||||
PackageDataEntryMatchModelRef::Installing(_) => None,
|
||||
PackageDataEntryMatchModelRef::Updating(a) => Some(a.as_installed()),
|
||||
PackageDataEntryMatchModelRef::Restoring(_) => None,
|
||||
PackageDataEntryMatchModelRef::Removing(_) => None,
|
||||
PackageDataEntryMatchModelRef::Installed(a) => Some(a.as_installed()),
|
||||
PackageDataEntryMatchModelRef::Error(_) => None,
|
||||
}
|
||||
}
|
||||
pub fn as_installed_mut(&mut self) -> Option<&mut Model<InstalledPackageInfo>> {
|
||||
match self.as_match_mut() {
|
||||
PackageDataEntryMatchModelMut::Installing(_) => None,
|
||||
PackageDataEntryMatchModelMut::Updating(a) => Some(a.as_installed_mut()),
|
||||
PackageDataEntryMatchModelMut::Restoring(_) => None,
|
||||
PackageDataEntryMatchModelMut::Removing(_) => None,
|
||||
PackageDataEntryMatchModelMut::Installed(a) => Some(a.as_installed_mut()),
|
||||
PackageDataEntryMatchModelMut::Error(_) => None,
|
||||
}
|
||||
}
|
||||
pub fn as_install_progress(&self) -> Option<&Model<Arc<InstallProgress>>> {
|
||||
match self.as_match() {
|
||||
PackageDataEntryMatchModelRef::Installing(a) => Some(a.as_install_progress()),
|
||||
PackageDataEntryMatchModelRef::Updating(a) => Some(a.as_install_progress()),
|
||||
PackageDataEntryMatchModelRef::Restoring(a) => Some(a.as_install_progress()),
|
||||
PackageDataEntryMatchModelRef::Removing(_) => None,
|
||||
PackageDataEntryMatchModelRef::Installed(_) => None,
|
||||
PackageDataEntryMatchModelRef::Error(_) => None,
|
||||
}
|
||||
}
|
||||
pub fn as_install_progress_mut(&mut self) -> Option<&mut Model<Arc<InstallProgress>>> {
|
||||
match self.as_match_mut() {
|
||||
PackageDataEntryMatchModelMut::Installing(a) => Some(a.as_install_progress_mut()),
|
||||
PackageDataEntryMatchModelMut::Updating(a) => Some(a.as_install_progress_mut()),
|
||||
PackageDataEntryMatchModelMut::Restoring(a) => Some(a.as_install_progress_mut()),
|
||||
PackageDataEntryMatchModelMut::Removing(_) => None,
|
||||
PackageDataEntryMatchModelMut::Installed(_) => None,
|
||||
PackageDataEntryMatchModelMut::Error(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct InstalledPackageInfo {
|
||||
pub status: Status,
|
||||
pub marketplace_url: Option<Url>,
|
||||
#[serde(default)]
|
||||
#[serde(with = "crate::util::serde::ed25519_pubkey")]
|
||||
pub developer_key: ed25519_dalek::VerifyingKey,
|
||||
pub manifest: Manifest,
|
||||
pub last_backup: Option<DateTime<Utc>>,
|
||||
pub dependency_info: BTreeMap<PackageId, StaticDependencyInfo>,
|
||||
pub current_dependents: CurrentDependents,
|
||||
pub current_dependencies: CurrentDependencies,
|
||||
pub interface_addresses: InterfaceAddressMap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
pub struct CurrentDependents(pub BTreeMap<PackageId, CurrentDependencyInfo>);
|
||||
impl CurrentDependents {
|
||||
pub fn map(
|
||||
mut self,
|
||||
transform: impl Fn(
|
||||
BTreeMap<PackageId, CurrentDependencyInfo>,
|
||||
) -> BTreeMap<PackageId, CurrentDependencyInfo>,
|
||||
) -> Self {
|
||||
self.0 = transform(self.0);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl Map for CurrentDependents {
|
||||
type Key = PackageId;
|
||||
type Value = CurrentDependencyInfo;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
pub struct CurrentDependencies(pub BTreeMap<PackageId, CurrentDependencyInfo>);
|
||||
impl CurrentDependencies {
|
||||
pub fn map(
|
||||
mut self,
|
||||
transform: impl Fn(
|
||||
BTreeMap<PackageId, CurrentDependencyInfo>,
|
||||
) -> BTreeMap<PackageId, CurrentDependencyInfo>,
|
||||
) -> Self {
|
||||
self.0 = transform(self.0);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl Map for CurrentDependencies {
|
||||
type Key = PackageId;
|
||||
type Value = CurrentDependencyInfo;
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct StaticDependencyInfo {
|
||||
pub title: String,
|
||||
pub icon: DataUrl<'static>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct CurrentDependencyInfo {
|
||||
#[serde(default)]
|
||||
pub pointers: BTreeSet<PackagePointerSpec>,
|
||||
pub health_checks: BTreeSet<HealthCheckId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InterfaceAddressMap(pub BTreeMap<InterfaceId, InterfaceAddresses>);
|
||||
impl Map for InterfaceAddressMap {
|
||||
type Key = InterfaceId;
|
||||
type Value = InterfaceAddresses;
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct InterfaceAddresses {
|
||||
pub tor_address: Option<String>,
|
||||
pub lan_address: Option<String>,
|
||||
}
|
||||
22
core/startos/src/db/package.rs
Normal file
22
core/startos/src/db/package.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use models::Version;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
|
||||
pub fn get_packages(db: Peeked) -> Result<Vec<(PackageId, Version)>, Error> {
|
||||
Ok(db
|
||||
.as_package_data()
|
||||
.keys()?
|
||||
.into_iter()
|
||||
.flat_map(|package_id| {
|
||||
let version = db
|
||||
.as_package_data()
|
||||
.as_idx(&package_id)?
|
||||
.as_manifest()
|
||||
.as_version()
|
||||
.de()
|
||||
.ok()?;
|
||||
Some((package_id, version))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
382
core/startos/src/db/prelude.rs
Normal file
382
core/startos/src/db/prelude.rs
Normal file
@@ -0,0 +1,382 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::marker::PhantomData;
|
||||
use std::panic::UnwindSafe;
|
||||
|
||||
use patch_db::value::InternedString;
|
||||
pub use patch_db::{HasModel, PatchDb, Value};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::db::model::DatabaseModel;
|
||||
use crate::prelude::*;
|
||||
|
||||
pub type Peeked = Model<super::model::Database>;
|
||||
|
||||
pub fn to_value<T>(value: &T) -> Result<Value, Error>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
patch_db::value::to_value(value).with_kind(ErrorKind::Serialization)
|
||||
}
|
||||
|
||||
pub fn from_value<T>(value: Value) -> Result<T, Error>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
patch_db::value::from_value(value).with_kind(ErrorKind::Deserialization)
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait PatchDbExt {
|
||||
async fn peek(&self) -> DatabaseModel;
|
||||
async fn mutate<U: UnwindSafe + Send>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut DatabaseModel) -> Result<U, Error> + UnwindSafe + Send,
|
||||
) -> Result<U, Error>;
|
||||
async fn map_mutate(
|
||||
&self,
|
||||
f: impl FnOnce(DatabaseModel) -> Result<DatabaseModel, Error> + UnwindSafe + Send,
|
||||
) -> Result<DatabaseModel, Error>;
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl PatchDbExt for PatchDb {
|
||||
async fn peek(&self) -> DatabaseModel {
|
||||
DatabaseModel::from(self.dump().await.value)
|
||||
}
|
||||
async fn mutate<U: UnwindSafe + Send>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut DatabaseModel) -> Result<U, Error> + UnwindSafe + Send,
|
||||
) -> Result<U, Error> {
|
||||
Ok(self
|
||||
.apply_function(|mut v| {
|
||||
let model = <&mut DatabaseModel>::from(&mut v);
|
||||
let res = f(model)?;
|
||||
Ok::<_, Error>((v, res))
|
||||
})
|
||||
.await?
|
||||
.1)
|
||||
}
|
||||
async fn map_mutate(
|
||||
&self,
|
||||
f: impl FnOnce(DatabaseModel) -> Result<DatabaseModel, Error> + UnwindSafe + Send,
|
||||
) -> Result<DatabaseModel, Error> {
|
||||
Ok(DatabaseModel::from(
|
||||
self.apply_function(|v| f(DatabaseModel::from(v)).map(|a| (a.into(), ())))
|
||||
.await?
|
||||
.0,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// &mut Model<T> <=> &mut Value
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug)]
|
||||
pub struct Model<T> {
|
||||
value: Value,
|
||||
phantom: PhantomData<T>,
|
||||
}
|
||||
impl<T: DeserializeOwned> Model<T> {
|
||||
pub fn de(&self) -> Result<T, Error> {
|
||||
from_value(self.value.clone())
|
||||
}
|
||||
}
|
||||
impl<T: Serialize> Model<T> {
|
||||
pub fn new(value: &T) -> Result<Self, Error> {
|
||||
Ok(Self::from(to_value(value)?))
|
||||
}
|
||||
pub fn ser(&mut self, value: &T) -> Result<(), Error> {
|
||||
self.value = to_value(value)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize + DeserializeOwned> Model<T> {
|
||||
pub fn replace(&mut self, value: &T) -> Result<T, Error> {
|
||||
let orig = self.de()?;
|
||||
self.ser(value)?;
|
||||
Ok(orig)
|
||||
}
|
||||
}
|
||||
impl<T> Clone for Model<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
value: self.value.clone(),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> From<Value> for Model<T> {
|
||||
fn from(value: Value) -> Self {
|
||||
Self {
|
||||
value,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> From<Model<T>> for Value {
|
||||
fn from(value: Model<T>) -> Self {
|
||||
value.value
|
||||
}
|
||||
}
|
||||
impl<'a, T> From<&'a Value> for &'a Model<T> {
|
||||
fn from(value: &'a Value) -> Self {
|
||||
unsafe { std::mem::transmute(value) }
|
||||
}
|
||||
}
|
||||
impl<'a, T> From<&'a Model<T>> for &'a Value {
|
||||
fn from(value: &'a Model<T>) -> Self {
|
||||
unsafe { std::mem::transmute(value) }
|
||||
}
|
||||
}
|
||||
impl<'a, T> From<&'a mut Value> for &mut Model<T> {
|
||||
fn from(value: &'a mut Value) -> Self {
|
||||
unsafe { std::mem::transmute(value) }
|
||||
}
|
||||
}
|
||||
impl<'a, T> From<&'a mut Model<T>> for &mut Value {
|
||||
fn from(value: &'a mut Model<T>) -> Self {
|
||||
unsafe { std::mem::transmute(value) }
|
||||
}
|
||||
}
|
||||
impl<T> patch_db::Model<T> for Model<T> {
|
||||
type Model<U> = Model<U>;
|
||||
}
|
||||
|
||||
impl<T> Model<Option<T>> {
|
||||
pub fn transpose(self) -> Option<Model<T>> {
|
||||
use patch_db::ModelExt;
|
||||
if self.value.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(self.transmute(|a| a))
|
||||
}
|
||||
}
|
||||
pub fn transpose_ref(&self) -> Option<&Model<T>> {
|
||||
use patch_db::ModelExt;
|
||||
if self.value.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(self.transmute_ref(|a| a))
|
||||
}
|
||||
}
|
||||
pub fn transpose_mut(&mut self) -> Option<&mut Model<T>> {
|
||||
use patch_db::ModelExt;
|
||||
if self.value.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(self.transmute_mut(|a| a))
|
||||
}
|
||||
}
|
||||
pub fn from_option(opt: Option<Model<T>>) -> Self {
|
||||
use patch_db::ModelExt;
|
||||
match opt {
|
||||
Some(a) => a.transmute(|a| a),
|
||||
None => Self::from_value(Value::Null),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Map: DeserializeOwned + Serialize {
|
||||
type Key;
|
||||
type Value;
|
||||
}
|
||||
|
||||
impl<A, B> Map for BTreeMap<A, B>
|
||||
where
|
||||
A: serde::Serialize + serde::de::DeserializeOwned + Ord,
|
||||
B: serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
type Key = A;
|
||||
type Value = B;
|
||||
}
|
||||
|
||||
impl<T: Map> Model<T>
|
||||
where
|
||||
T::Key: AsRef<str>,
|
||||
T::Value: Serialize,
|
||||
{
|
||||
pub fn insert(&mut self, key: &T::Key, value: &T::Value) -> Result<(), Error> {
|
||||
use serde::ser::Error;
|
||||
let v = patch_db::value::to_value(value)?;
|
||||
match &mut self.value {
|
||||
Value::Object(o) => {
|
||||
o.insert(InternedString::intern(key.as_ref()), v);
|
||||
Ok(())
|
||||
}
|
||||
v => Err(patch_db::value::Error {
|
||||
source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")),
|
||||
kind: patch_db::value::ErrorKind::Serialization,
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
pub fn insert_model(&mut self, key: &T::Key, value: Model<T::Value>) -> Result<(), Error> {
|
||||
use patch_db::ModelExt;
|
||||
use serde::ser::Error;
|
||||
let v = value.into_value();
|
||||
match &mut self.value {
|
||||
Value::Object(o) => {
|
||||
o.insert(InternedString::intern(key.as_ref()), v);
|
||||
Ok(())
|
||||
}
|
||||
v => Err(patch_db::value::Error {
|
||||
source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")),
|
||||
kind: patch_db::value::ErrorKind::Serialization,
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Map> Model<T>
|
||||
where
|
||||
T::Key: DeserializeOwned + Ord + Clone,
|
||||
{
|
||||
pub fn keys(&self) -> Result<Vec<T::Key>, Error> {
|
||||
use serde::de::Error;
|
||||
use serde::Deserialize;
|
||||
match &self.value {
|
||||
Value::Object(o) => o
|
||||
.keys()
|
||||
.cloned()
|
||||
.map(|k| {
|
||||
T::Key::deserialize(patch_db::value::de::InternedStringDeserializer::from(k))
|
||||
.map_err(|e| {
|
||||
patch_db::value::Error {
|
||||
kind: patch_db::value::ErrorKind::Deserialization,
|
||||
source: e,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
v => Err(patch_db::value::Error {
|
||||
source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")),
|
||||
kind: patch_db::value::ErrorKind::Deserialization,
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_entries(self) -> Result<Vec<(T::Key, Model<T::Value>)>, Error> {
|
||||
use patch_db::ModelExt;
|
||||
use serde::de::Error;
|
||||
use serde::Deserialize;
|
||||
match self.value {
|
||||
Value::Object(o) => o
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
Ok((
|
||||
T::Key::deserialize(patch_db::value::de::InternedStringDeserializer::from(
|
||||
k,
|
||||
))
|
||||
.with_kind(ErrorKind::Deserialization)?,
|
||||
Model::from_value(v),
|
||||
))
|
||||
})
|
||||
.collect(),
|
||||
v => Err(patch_db::value::Error {
|
||||
source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")),
|
||||
kind: patch_db::value::ErrorKind::Deserialization,
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
pub fn as_entries(&self) -> Result<Vec<(T::Key, &Model<T::Value>)>, Error> {
|
||||
use patch_db::ModelExt;
|
||||
use serde::de::Error;
|
||||
use serde::Deserialize;
|
||||
match &self.value {
|
||||
Value::Object(o) => o
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
Ok((
|
||||
T::Key::deserialize(patch_db::value::de::InternedStringDeserializer::from(
|
||||
k.clone(),
|
||||
))
|
||||
.with_kind(ErrorKind::Deserialization)?,
|
||||
Model::value_as(v),
|
||||
))
|
||||
})
|
||||
.collect(),
|
||||
v => Err(patch_db::value::Error {
|
||||
source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")),
|
||||
kind: patch_db::value::ErrorKind::Deserialization,
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
pub fn as_entries_mut(&mut self) -> Result<Vec<(T::Key, &mut Model<T::Value>)>, Error> {
|
||||
use patch_db::ModelExt;
|
||||
use serde::de::Error;
|
||||
use serde::Deserialize;
|
||||
match &mut self.value {
|
||||
Value::Object(o) => o
|
||||
.iter_mut()
|
||||
.map(|(k, v)| {
|
||||
Ok((
|
||||
T::Key::deserialize(patch_db::value::de::InternedStringDeserializer::from(
|
||||
k.clone(),
|
||||
))
|
||||
.with_kind(ErrorKind::Deserialization)?,
|
||||
Model::value_as_mut(v),
|
||||
))
|
||||
})
|
||||
.collect(),
|
||||
v => Err(patch_db::value::Error {
|
||||
source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")),
|
||||
kind: patch_db::value::ErrorKind::Deserialization,
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T: Map> Model<T>
|
||||
where
|
||||
T::Key: AsRef<str>,
|
||||
{
|
||||
pub fn into_idx(self, key: &T::Key) -> Option<Model<T::Value>> {
|
||||
use patch_db::ModelExt;
|
||||
match &self.value {
|
||||
Value::Object(o) if o.contains_key(key.as_ref()) => Some(self.transmute(|v| {
|
||||
use patch_db::value::index::Index;
|
||||
key.as_ref().index_into_owned(v).unwrap()
|
||||
})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn as_idx<'a>(&'a self, key: &T::Key) -> Option<&'a Model<T::Value>> {
|
||||
use patch_db::ModelExt;
|
||||
match &self.value {
|
||||
Value::Object(o) if o.contains_key(key.as_ref()) => Some(self.transmute_ref(|v| {
|
||||
use patch_db::value::index::Index;
|
||||
key.as_ref().index_into(v).unwrap()
|
||||
})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn as_idx_mut<'a>(&'a mut self, key: &T::Key) -> Option<&'a mut Model<T::Value>> {
|
||||
use patch_db::ModelExt;
|
||||
match &mut self.value {
|
||||
Value::Object(o) if o.contains_key(key.as_ref()) => Some(self.transmute_mut(|v| {
|
||||
use patch_db::value::index::Index;
|
||||
key.as_ref().index_or_insert(v)
|
||||
})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn remove(&mut self, key: &T::Key) -> Result<Option<Model<T::Value>>, Error> {
|
||||
use serde::ser::Error;
|
||||
match &mut self.value {
|
||||
Value::Object(o) => {
|
||||
let v = o.remove(key.as_ref());
|
||||
Ok(v.map(patch_db::ModelExt::from_value))
|
||||
}
|
||||
v => Err(patch_db::value::Error {
|
||||
source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")),
|
||||
kind: patch_db::value::ErrorKind::Serialization,
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user