From 1ba6cf79c076d6d192366a12b75ff6f977a0cb32 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Sat, 24 Aug 2024 14:55:26 +0200 Subject: [PATCH] feat: fix minor bugs Signed-off-by: kjuulh --- .../examples/basic/plan/cuddle.plan.toml | 1 + .../cuddle/examples/basic/project/cuddle.toml | 3 + .../examples/schema/plan/cuddle.plan.toml | 2 + .../cuddle/examples/schema/plan/cuddle.toml | 0 crates/cuddle/examples/schema/plan/schema.ncl | 5 + crates/cuddle/src/lib.rs | 4 + crates/cuddle/src/main.rs | 42 +++++-- crates/cuddle/src/plan.rs | 111 +++++++++++++++++- crates/cuddle/src/project.rs | 72 ++++++++++-- crates/cuddle/src/schema_validator.rs | 30 +++++ crates/cuddle/src/schema_validator/nickel.rs | 111 ++++++++++++++++++ crates/cuddle/src/state.rs | 45 +++++++ 12 files changed, 405 insertions(+), 21 deletions(-) create mode 100644 crates/cuddle/examples/schema/plan/cuddle.plan.toml create mode 100644 crates/cuddle/examples/schema/plan/cuddle.toml create mode 100644 crates/cuddle/examples/schema/plan/schema.ncl create mode 100644 crates/cuddle/src/lib.rs create mode 100644 crates/cuddle/src/schema_validator.rs create mode 100644 crates/cuddle/src/schema_validator/nickel.rs create mode 100644 crates/cuddle/src/state.rs diff --git a/crates/cuddle/examples/basic/plan/cuddle.plan.toml b/crates/cuddle/examples/basic/plan/cuddle.plan.toml index e69de29..fe93058 100644 --- a/crates/cuddle/examples/basic/plan/cuddle.plan.toml +++ b/crates/cuddle/examples/basic/plan/cuddle.plan.toml @@ -0,0 +1 @@ +[plan] diff --git a/crates/cuddle/examples/basic/project/cuddle.toml b/crates/cuddle/examples/basic/project/cuddle.toml index 244b7b7..098c63d 100644 --- a/crates/cuddle/examples/basic/project/cuddle.toml +++ b/crates/cuddle/examples/basic/project/cuddle.toml @@ -1,2 +1,5 @@ [plan] path = "../plan" + +[project] +name = "basic" diff --git a/crates/cuddle/examples/schema/plan/cuddle.plan.toml b/crates/cuddle/examples/schema/plan/cuddle.plan.toml new file mode 100644 index 0000000..fa216c9 --- /dev/null +++ b/crates/cuddle/examples/schema/plan/cuddle.plan.toml @@ -0,0 +1,2 @@ +[plan] +schema = { nickel = "schema.ncl" } diff --git a/crates/cuddle/examples/schema/plan/cuddle.toml b/crates/cuddle/examples/schema/plan/cuddle.toml new file mode 100644 index 0000000..e69de29 diff --git a/crates/cuddle/examples/schema/plan/schema.ncl b/crates/cuddle/examples/schema/plan/schema.ncl new file mode 100644 index 0000000..d670119 --- /dev/null +++ b/crates/cuddle/examples/schema/plan/schema.ncl @@ -0,0 +1,5 @@ +{ + ProjectSchema = { + name | String + } +} diff --git a/crates/cuddle/src/lib.rs b/crates/cuddle/src/lib.rs new file mode 100644 index 0000000..f8813eb --- /dev/null +++ b/crates/cuddle/src/lib.rs @@ -0,0 +1,4 @@ +mod plan; +mod project; +mod schema_validator; +mod state; diff --git a/crates/cuddle/src/main.rs b/crates/cuddle/src/main.rs index 5e42e86..2940fff 100644 --- a/crates/cuddle/src/main.rs +++ b/crates/cuddle/src/main.rs @@ -1,8 +1,11 @@ -use plan::Plan; +use plan::{ClonedPlan, Plan}; use project::ProjectPlan; +use state::ValidatedState; mod plan; mod project; +mod schema_validator; +mod state; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -13,6 +16,8 @@ async fn main() -> anyhow::Result<()> { .prepare_project() .await? .prepare_plan() + .await? + .build_state() .await?; Ok(()) @@ -25,7 +30,7 @@ struct PrepareProject { struct PreparePlan { project: Option, - plan: Option, + plan: Option, } struct Cuddle { @@ -54,13 +59,32 @@ impl Cuddle { impl Cuddle { pub async fn prepare_plan(&self) -> anyhow::Result> { - if let Some(project) = &self.state.project { - match Plan::new().clone_from_project(project).await? { - Some(plan) => todo!(), - None => todo!(), - } - } + let plan = if let Some(project) = &self.state.project { + Plan::new().clone_from_project(project).await? + } else { + None + }; - todo!() + Ok(Cuddle { + state: PreparePlan { + project: self.state.project.clone(), + plan, + }, + }) + } +} + +impl Cuddle { + pub async fn build_state(&self) -> anyhow::Result> { + let state = if let Some(project) = &self.state.project { + let state = state::State::new(); + let raw_state = state.build_state(project, &self.state.plan).await?; + + state.validate_state(&raw_state).await? + } else { + ValidatedState {} + }; + + Ok(Cuddle { state }) } } diff --git a/crates/cuddle/src/plan.rs b/crates/cuddle/src/plan.rs index 66c0183..f00e1d5 100644 --- a/crates/cuddle/src/plan.rs +++ b/crates/cuddle/src/plan.rs @@ -1,11 +1,13 @@ use std::path::{Path, PathBuf}; use fs_extra::dir::CopyOptions; +use serde::Deserialize; use crate::project::{self, ProjectPlan}; pub const CUDDLE_PLAN_FOLDER: &str = "plan"; pub const CUDDLE_PROJECT_WORKSPACE: &str = ".cuddle"; +pub const CUDDLE_PLAN_FILE: &str = "cuddle.plan.toml"; pub trait PlanPathExt { fn plan_path(&self) -> PathBuf; @@ -19,8 +21,60 @@ impl PlanPathExt for project::ProjectPlan { } } -pub struct Plan {} +pub struct RawPlan { + pub config: RawPlanConfig, + pub root: PathBuf, +} +impl RawPlan { + pub fn new(config: RawPlanConfig, root: &Path) -> Self { + Self { + config, + root: root.to_path_buf(), + } + } + + pub fn from_file(content: &str, root: &Path) -> anyhow::Result { + let config: RawPlanConfig = toml::from_str(content)?; + + Ok(Self::new(config, root)) + } + + pub async fn from_path(path: &Path) -> anyhow::Result { + let cuddle_file = path.join(CUDDLE_PLAN_FILE); + + tracing::trace!( + path = cuddle_file.display().to_string(), + "searching for cuddle.toml project file" + ); + + if !cuddle_file.exists() { + anyhow::bail!("no cuddle.toml project file found"); + } + + let cuddle_plan_file = tokio::fs::read_to_string(cuddle_file).await?; + + Self::from_file(&cuddle_plan_file, path) + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct RawPlanConfig { + pub plan: RawPlanConfigSection, +} +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct RawPlanConfigSection { + pub schema: Option, +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum RawPlanSchema { + Nickel { nickel: PathBuf }, + JsonSchema { jsonschema: String }, +} + +pub struct Plan {} impl Plan { pub fn new() -> Self { Self {} @@ -113,8 +167,61 @@ impl Plan { .copy_inside(false), )?; - todo!() + Ok(ClonedPlan {}) } } pub struct ClonedPlan {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_can_parse_schema_plan() -> anyhow::Result<()> { + let plan = RawPlan::from_file( + r##" + [plan] + schema = {nickel = "contract.ncl"} + "##, + &PathBuf::new(), + )?; + + assert_eq!( + RawPlanConfig { + plan: RawPlanConfigSection { + schema: Some(RawPlanSchema::Nickel { + nickel: "contract.ncl".into() + }), + } + }, + plan.config, + ); + + Ok(()) + } + + #[test] + fn test_can_parse_json_schema() -> anyhow::Result<()> { + let plan = RawPlan::from_file( + r##" + [plan] + schema = {jsonschema = "schema.json"} + "##, + &PathBuf::new(), + )?; + + assert_eq!( + RawPlanConfig { + plan: RawPlanConfigSection { + schema: Some(RawPlanSchema::JsonSchema { + jsonschema: "schema.json".into() + }), + } + }, + plan.config, + ); + + Ok(()) + } +} diff --git a/crates/cuddle/src/project.rs b/crates/cuddle/src/project.rs index 9443ce4..1c2ed76 100644 --- a/crates/cuddle/src/project.rs +++ b/crates/cuddle/src/project.rs @@ -1,28 +1,70 @@ -use std::{env::current_dir, path::PathBuf}; +use std::{ + env::current_dir, + path::{Path, PathBuf}, +}; use serde::Deserialize; -const CUDDLE_FILE_NAME: &str = "cuddle.toml"; +pub const CUDDLE_PROJECT_FILE: &str = "cuddle.toml"; +#[derive(Clone)] +pub struct RawProject { + config: RawConfig, + pub root: PathBuf, +} + +impl RawProject { + pub fn new(config: RawConfig, root: &Path) -> Self { + Self { + config, + root: root.to_path_buf(), + } + } + + pub fn from_file(content: &str, root: &Path) -> anyhow::Result { + let config: RawConfig = toml::from_str(content)?; + + Ok(Self::new(config, root)) + } + + pub async fn from_path(path: &Path) -> anyhow::Result { + let cuddle_file = path.join(CUDDLE_PROJECT_FILE); + + tracing::trace!( + path = cuddle_file.display().to_string(), + "searching for cuddle.toml project file" + ); + + if !cuddle_file.exists() { + anyhow::bail!("no cuddle.toml project file found"); + } + + let cuddle_project_file = tokio::fs::read_to_string(cuddle_file).await?; + + Self::from_file(&cuddle_project_file, path) + } +} + +#[derive(Clone)] pub struct ProjectPlan { - config: Config, + config: ProjectPlanConfig, pub root: PathBuf, } impl ProjectPlan { - pub fn new(config: Config, root: PathBuf) -> Self { + pub fn new(config: ProjectPlanConfig, root: PathBuf) -> Self { Self { config, root } } pub fn from_file(content: &str, root: PathBuf) -> anyhow::Result { - let config: Config = toml::from_str(&content)?; + let config: ProjectPlanConfig = toml::from_str(content)?; Ok(Self::new(config, root)) } pub async fn from_current_path() -> anyhow::Result> { let cur_dir = current_dir()?; - let cuddle_file = cur_dir.join(CUDDLE_FILE_NAME); + let cuddle_file = cur_dir.join(CUDDLE_PROJECT_FILE); tracing::trace!( path = cuddle_file.display().to_string(), @@ -60,7 +102,17 @@ pub enum Plan { } #[derive(Debug, Clone, Deserialize, PartialEq)] -pub struct Config { +pub struct RawConfig { + project: ProjectConfig, +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct ProjectConfig { + name: String, +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct ProjectPlanConfig { plan: Option, } @@ -87,7 +139,7 @@ git = "https://github.com/kjuulh/some-cuddle-project" )?; assert_eq!( - Config { + ProjectPlanConfig { plan: Some(PlanConfig::Git { git: "https://github.com/kjuulh/some-cuddle-project".into() }) @@ -108,7 +160,7 @@ plan = "https://github.com/kjuulh/some-cuddle-project" )?; assert_eq!( - Config { + ProjectPlanConfig { plan: Some(PlanConfig::Bare( "https://github.com/kjuulh/some-cuddle-project".into() )) @@ -123,7 +175,7 @@ plan = "https://github.com/kjuulh/some-cuddle-project" fn test_can_parse_simple_file_none() -> anyhow::Result<()> { let project = ProjectPlan::from_file(r##""##, PathBuf::new())?; - assert_eq!(Config { plan: None }, project.config); + assert_eq!(ProjectPlanConfig { plan: None }, project.config); Ok(()) } diff --git a/crates/cuddle/src/schema_validator.rs b/crates/cuddle/src/schema_validator.rs new file mode 100644 index 0000000..93c8eb7 --- /dev/null +++ b/crates/cuddle/src/schema_validator.rs @@ -0,0 +1,30 @@ +use nickel::NickelSchemaValidator; + +use crate::{ + plan::{RawPlan, RawPlanSchema}, + project::RawProject, +}; + +mod nickel; + +pub struct SchemaValidator {} + +impl SchemaValidator { + pub fn new() -> Self { + Self {} + } + + pub fn validate(&self, plan: &RawPlan, project: &RawProject) -> anyhow::Result> { + let schema = match &plan.config.plan.schema { + Some(schema) => schema, + None => return Ok(None), + }; + + match schema { + RawPlanSchema::Nickel { nickel } => Ok(Some(NickelSchemaValidator::validate( + plan, project, nickel, + )?)), + RawPlanSchema::JsonSchema { jsonschema } => todo!("jsonschema not implemented yet"), + } + } +} diff --git a/crates/cuddle/src/schema_validator/nickel.rs b/crates/cuddle/src/schema_validator/nickel.rs new file mode 100644 index 0000000..3f28d14 --- /dev/null +++ b/crates/cuddle/src/schema_validator/nickel.rs @@ -0,0 +1,111 @@ +use std::{ + env::temp_dir, + path::{Path, PathBuf}, +}; + +use uuid::Uuid; + +use crate::{ + plan::RawPlan, + project::{RawProject, CUDDLE_PROJECT_FILE}, +}; + +pub trait NickelPlanExt { + fn schema_path(&self, schema: &Path) -> PathBuf; +} + +impl NickelPlanExt for RawPlan { + fn schema_path(&self, schema: &Path) -> PathBuf { + self.root.join(schema) + } +} + +pub trait NickelProjectExt { + fn project_path(&self) -> PathBuf; +} + +impl NickelProjectExt for RawProject { + fn project_path(&self) -> PathBuf { + self.root.join(CUDDLE_PROJECT_FILE) + } +} + +fn unique_contract_file() -> anyhow::Result { + let p = temp_dir() + .join("cuddle") + .join("nickel-contracts") + .join(Uuid::new_v4().to_string()); + + std::fs::create_dir_all(&p)?; + + let file = p.join("contract.ncl"); + + Ok(TempDirGuard { dir: p, file }) +} + +pub struct TempDirGuard { + dir: PathBuf, + file: PathBuf, +} + +impl Drop for TempDirGuard { + fn drop(&mut self) { + if let Err(e) = std::fs::remove_dir_all(&self.dir) { + panic!("failed to remove tempdir: {}", e) + } + } +} + +impl std::ops::Deref for TempDirGuard { + type Target = PathBuf; + + fn deref(&self) -> &Self::Target { + &self.file + } +} + +pub struct NickelSchemaValidator {} +impl NickelSchemaValidator { + pub fn validate(plan: &RawPlan, project: &RawProject, nickel: &Path) -> anyhow::Result<()> { + let nickel_file = plan.schema_path(nickel); + let cuddle_file = project.project_path(); + + let nickel_file = format!( + r##" +let {{ProjectSchema, ..}} = import "{}" in + +let Schema = {{ + project | ProjectSchema, .. +}} in + +{{ + + config | Schema = import "{}" +}} + +"##, + nickel_file.display(), + cuddle_file.display() + ); + + let contract_file = unique_contract_file()?; + + std::fs::write(contract_file.as_path(), nickel_file)?; + + let mut cmd = std::process::Command::new("nickel"); + + cmd.args(["export", &contract_file.display().to_string()]); + + let output = cmd.output()?; + + if !output.status.success() { + anyhow::bail!( + "failed to run nickel command: output: {} {}", + std::str::from_utf8(&output.stdout)?, + std::str::from_utf8(&output.stderr)? + ) + } + + Ok(()) + } +} diff --git a/crates/cuddle/src/state.rs b/crates/cuddle/src/state.rs new file mode 100644 index 0000000..32ddef5 --- /dev/null +++ b/crates/cuddle/src/state.rs @@ -0,0 +1,45 @@ +use crate::{ + plan::{self, ClonedPlan, PlanPathExt}, + project::{self, ProjectPlan}, + schema_validator::SchemaValidator, +}; + +pub struct State {} +impl State { + pub fn new() -> Self { + Self {} + } + + pub async fn build_state( + &self, + project_plan: &ProjectPlan, + cloned_plan: &Option, + ) -> anyhow::Result { + let project = project::RawProject::from_path(&project_plan.root).await?; + let plan = if let Some(_cloned_plan) = cloned_plan { + Some(plan::RawPlan::from_path(&project_plan.plan_path()).await?) + } else { + None + }; + + Ok(RawState { project, plan }) + } + + pub async fn validate_state(&self, state: &RawState) -> anyhow::Result { + // 2. Prepare context for actions and components + + if let Some(plan) = &state.plan { + SchemaValidator::new().validate(plan, &state.project)?; + } + + // 3. Match against schema from plan + + Ok(ValidatedState {}) + } +} + +pub struct RawState { + project: project::RawProject, + plan: Option, +} +pub struct ValidatedState {}