diff --git a/Cargo.toml b/Cargo.toml index 96275ae..ddd4218 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,21 @@ [package] name = "fishy" version = "0.1.0" -authors = [ - "adz ", - "sandreae ", -] +authors = ["adz ", "sandreae "] edition = "2021" description = "Create, manage and deploy p2panda schemas" repository = "https://github.com/p2panda/fishy" license = "AGPL-3.0-or-later" readme = "README.md" +[[bin]] +name = "fishy" +path = "src/main.rs" + +[lib] +name = "fishy" +path = "src/lib/lib.rs" + [profile.release] strip = true opt-level = "z" diff --git a/src/commands/build/mod.rs b/src/commands/build.rs similarity index 84% rename from src/commands/build/mod.rs rename to src/commands/build.rs index 912246d..48ffa1d 100644 --- a/src/commands/build/mod.rs +++ b/src/commands/build.rs @@ -1,28 +1,19 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -mod current; -mod diff; -mod executor; -mod previous; -mod print; -mod write; - use std::path::PathBuf; use anyhow::{bail, Context, Result}; use dialoguer::Confirm; +use fishy::build::{ + execute_plan, get_current_schemas, get_diff, get_previous_schemas, write_to_lock_file, +}; +use fishy::lock_file::LockFile; +use fishy::schema_file::SchemaFile; +use fishy::utils::files::absolute_path; +use fishy::utils::key_pair; use p2panda_rs::test_utils::memory_store::MemoryStore; -use crate::commands::build::current::get_current_schemas; -use crate::commands::build::diff::get_diff; -use crate::commands::build::executor::execute_plan; -use crate::commands::build::previous::get_previous_schemas; -use crate::commands::build::print::print_plan; -use crate::commands::build::write::write_to_lock_file; -use crate::lock_file::LockFile; -use crate::schema_file::SchemaFile; -use crate::utils::files::absolute_path; -use crate::utils::key_pair; +use crate::utils::print::print_plan; use crate::utils::terminal::{print_title, print_variable}; /// Automatically creates and signs p2panda data from a key pair and the defined schemas. diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 39e2872..48499c6 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -2,18 +2,13 @@ use std::path::PathBuf; -use anyhow::{anyhow, bail, Context, Result}; -use gql_client::Client; +use anyhow::{bail, Context, Result}; +use fishy::lock_file::LockFile; +use fishy::Client; use indicatif::ProgressBar; -use p2panda_rs::entry::decode::decode_entry; -use p2panda_rs::entry::traits::AsEntry; -use p2panda_rs::entry::{LogId, SeqNum}; -use p2panda_rs::hash::Hash; -use serde::Deserialize; -use crate::lock_file::LockFile; -use crate::utils::files::absolute_path; use crate::utils::terminal::{print_title, print_variable}; +use fishy::utils::files::absolute_path; /// Deploy created schemas on a node. pub async fn deploy(lock_path: PathBuf, endpoint: &str) -> Result<()> { @@ -27,7 +22,7 @@ pub async fn deploy(lock_path: PathBuf, endpoint: &str) -> Result<()> { lock_path.display() ))?; - let commits = lock_file.commits.unwrap_or(Vec::new()); + let commits = lock_file.commits(); if commits.is_empty() { bail!("No data given to deploy to node. Please run `update` command first."); } @@ -41,61 +36,12 @@ pub async fn deploy(lock_path: PathBuf, endpoint: &str) -> Result<()> { let client = Client::new(endpoint); for commit in commits { - let entry = decode_entry(&commit.entry).unwrap(); + let published = client.publish(commit).await?; - let query = format!( - r#" - {{ - nextArgs(publicKey: "{}", viewId: "{}") {{ - logId - seqNum - skiplink - backlink - }} - }} - "#, - entry.public_key(), - commit.entry_hash, - ); - - let response = client.query_unwrap::(&query).await; - - if let Ok(result) = response { - let args = result.next_args; - - if entry.log_id() != &args.log_id { - bail!("Inconsistency between local commits and node detected"); - } - - // Check if node already knows about this commit - if entry.seq_num() < &args.seq_num { - skipped += 1; - progress.inc(1); - - // Skip this one - continue; - } + if !published { + skipped += 1; } - let query = format!( - r#" - mutation Publish {{ - publish(entry: "{}", operation: "{}") {{ - logId - seqNum - skiplink - backlink - }} - }} - "#, - commit.entry, commit.operation - ); - - client - .query_unwrap::(&query) - .await - .map_err(|err| anyhow!("GraphQL request to node failed: {err}"))?; - progress.inc(1); } @@ -113,27 +59,3 @@ pub async fn deploy(lock_path: PathBuf, endpoint: &str) -> Result<()> { Ok(()) } - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -#[allow(dead_code)] -struct NextArguments { - log_id: LogId, - seq_num: SeqNum, - skiplink: Option, - backlink: Option, -} - -/// GraphQL response for `nextArgs` query. -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct NextArgsResponse { - next_args: NextArguments, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -#[allow(dead_code)] -struct PublishResponse { - publish: NextArguments, -} diff --git a/src/commands/init.rs b/src/commands/init.rs index eec3416..1e9e81c 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -4,12 +4,12 @@ use std::path::{Path, PathBuf}; use anyhow::{bail, Result}; use dialoguer::Input; +use fishy::utils::files::{absolute_path, write_file}; +use fishy::utils::key_pair::write_key_pair; use p2panda_rs::identity::KeyPair; use p2panda_rs::schema::validate::validate_name; use crate::constants::{PRIVATE_KEY_FILE_NAME, SCHEMA_FILE_NAME}; -use crate::utils::files::{absolute_path, write_file}; -use crate::utils::key_pair::write_key_pair; use crate::utils::terminal::{print_title, print_variable}; /// Initialises all files for a new fishy project in a given folder. diff --git a/src/commands/build/current.rs b/src/lib/build/current.rs similarity index 95% rename from src/commands/build/current.rs rename to src/lib/build/current.rs index 1c040f0..23f6dac 100644 --- a/src/commands/build/current.rs +++ b/src/lib/build/current.rs @@ -10,7 +10,7 @@ pub fn get_current_schemas(schema_file: &SchemaFile) -> Result Result { - // Sometimes `commits` is not defined in the .toml file, set an empty array as a fallback - let commits = lock_file.commits.clone().unwrap_or_default(); - // Publish every commit in our temporary, in-memory "node" to materialize schema documents - for commit in commits { + for commit in lock_file.commits() { // Check entry hash integrity if commit.entry_hash != commit.entry.hash() { bail!( diff --git a/src/commands/build/write.rs b/src/lib/build/write.rs similarity index 82% rename from src/commands/build/write.rs rename to src/lib/build/write.rs index d158b93..2807e53 100644 --- a/src/commands/build/write.rs +++ b/src/lib/build/write.rs @@ -10,17 +10,13 @@ use crate::utils::files::{self}; /// Write commits to lock file. pub fn write_to_lock_file( mut new_commits: Vec, - mut lock_file: LockFile, + lock_file: LockFile, lock_path: PathBuf, ) -> Result<()> { // Add new commits to the existing ones let applied_commits_count = new_commits.len(); - let mut commits: Vec = Vec::new(); - - if let Some(current_commits) = lock_file.commits.as_mut() { - commits.append(current_commits); - } + let mut commits: Vec = lock_file.commits(); commits.append(&mut new_commits); // Write everything to .toml file diff --git a/src/lib/client.rs b/src/lib/client.rs new file mode 100644 index 0000000..7f5acb7 --- /dev/null +++ b/src/lib/client.rs @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use anyhow::{anyhow, bail, Result}; +use gql_client::Client as GQLClient; +use p2panda_rs::entry::decode::decode_entry; +use p2panda_rs::entry::traits::AsEntry; +use p2panda_rs::entry::{LogId, SeqNum}; +use p2panda_rs::hash::Hash; +use serde::Deserialize; + +use crate::lock_file::Commit; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[allow(dead_code)] +struct NextArguments { + log_id: LogId, + seq_num: SeqNum, + skiplink: Option, + backlink: Option, +} + +/// GraphQL response for `nextArgs` query. +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct NextArgsResponse { + next_args: NextArguments, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[allow(dead_code)] +struct PublishResponse { + publish: NextArguments, +} + +pub struct Client(GQLClient); + +impl Client { + pub fn new(endpoint: &str) -> Self { + Self(GQLClient::new(endpoint)) + } + + pub async fn publish(&self, commit: Commit) -> Result { + let entry = decode_entry(&commit.entry).unwrap(); + + let query = format!( + r#" + {{ + nextArgs(publicKey: "{}", viewId: "{}") {{ + logId + seqNum + skiplink + backlink + }} + }} + "#, + entry.public_key(), + commit.entry_hash, + ); + + let response = self.0.query_unwrap::(&query).await; + + if let Ok(result) = response { + let args = result.next_args; + + if entry.log_id() != &args.log_id { + bail!("Inconsistency between local commits and node detected"); + } + + // Check if node already knows about this commit + if entry.seq_num() < &args.seq_num { + // Skip this one + return Ok(false); + } + } + + let query = format!( + r#" + mutation Publish {{ + publish(entry: "{}", operation: "{}") {{ + logId + seqNum + skiplink + backlink + }} + }} + "#, + commit.entry, commit.operation + ); + + self.0 + .query_unwrap::(&query) + .await + .map_err(|err| anyhow!("GraphQL request to node failed: {err}"))?; + + Ok(true) + } +} diff --git a/src/lib/lib.rs b/src/lib/lib.rs new file mode 100644 index 0000000..f175244 --- /dev/null +++ b/src/lib/lib.rs @@ -0,0 +1,7 @@ +pub mod build; +mod client; +pub mod lock_file; +pub mod schema_file; +pub mod utils; + +pub use client::Client; diff --git a/src/lock_file.rs b/src/lib/lock_file.rs similarity index 86% rename from src/lock_file.rs rename to src/lib/lock_file.rs index 639c2c8..7fbc146 100644 --- a/src/lock_file.rs +++ b/src/lib/lock_file.rs @@ -31,8 +31,8 @@ use crate::utils::files; #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct LockFile { - pub version: LockFileVersion, - pub commits: Option>, + version: LockFileVersion, + commits: Option>, } impl LockFile { @@ -52,6 +52,21 @@ impl LockFile { toml::from_str(&data).with_context(|| "Invalid TOML syntax in lock file")?; Ok(schema_file) } + + /// Version of the lockfile. + pub fn version(&self) -> &LockFileVersion { + &self.version + } + + /// Commits contained in the lockfile. + /// + /// Returns an empty vec if no `commits` were defined in the .toml file yet. + pub fn commits(&self) -> Vec { + match &self.commits { + Some(commits) => commits.clone(), + None => Vec::new(), + } + } } /// Known versions of lock file format. diff --git a/src/schema_file.rs b/src/lib/schema_file.rs similarity index 96% rename from src/schema_file.rs rename to src/lib/schema_file.rs index da58572..70080a6 100644 --- a/src/schema_file.rs +++ b/src/lib/schema_file.rs @@ -70,6 +70,11 @@ impl SchemaFields { self.0.len() } + /// Returns `true` if schema contains no fields, otherwise `false`. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// Inserts a new field. pub fn insert(&mut self, field_name: &FieldName, field: &SchemaField) { self.0.insert(field_name.clone(), field.clone()); @@ -81,6 +86,12 @@ impl SchemaFields { } } +impl Default for SchemaFields { + fn default() -> Self { + Self::new() + } +} + /// Definition of a single schema field. #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] #[serde(untagged, deny_unknown_fields)] diff --git a/src/utils/files.rs b/src/lib/utils/files.rs similarity index 100% rename from src/utils/files.rs rename to src/lib/utils/files.rs diff --git a/src/utils/key_pair.rs b/src/lib/utils/key_pair.rs similarity index 100% rename from src/utils/key_pair.rs rename to src/lib/utils/key_pair.rs diff --git a/src/lib/utils/mod.rs b/src/lib/utils/mod.rs new file mode 100644 index 0000000..5147879 --- /dev/null +++ b/src/lib/utils/mod.rs @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod files; +pub mod key_pair; diff --git a/src/main.rs b/src/main.rs index 3293973..7bd2fb0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,6 @@ mod commands; mod constants; -mod lock_file; -mod schema_file; mod utils; use std::path::PathBuf; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ea5e516..a14eda4 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,4 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pub mod files; -pub mod key_pair; +pub mod print; pub mod terminal; diff --git a/src/commands/build/print.rs b/src/utils/print.rs similarity index 98% rename from src/commands/build/print.rs rename to src/utils/print.rs index 77015b3..07a5cf2 100644 --- a/src/commands/build/print.rs +++ b/src/utils/print.rs @@ -12,13 +12,13 @@ use p2panda_rs::schema::{ FieldName, FieldType as PandaFieldType, SchemaDescription, SchemaId, SchemaName, }; -use crate::schema_file::{ +use fishy::schema_file::{ FieldType, RelationId, RelationSchema, RelationType, SchemaField, SchemaFields, }; -use super::diff::FieldTypeDiff; -use super::executor::Plan; -use super::previous::PreviousSchemas; +use fishy::build::FieldTypeDiff; +use fishy::build::Plan; +use fishy::build::PreviousSchemas; /// Shows the execution plan to the user. pub fn print_plan(