feat: fix minor bugs

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
Kasper Juul Hermansen 2024-08-24 14:55:26 +02:00
parent c2b7e44ea3
commit 1ba6cf79c0
Signed by: kjuulh
GPG Key ID: D85D7535F18F35FA
12 changed files with 405 additions and 21 deletions

View File

@ -0,0 +1 @@
[plan]

View File

@ -1,2 +1,5 @@
[plan]
path = "../plan"
[project]
name = "basic"

View File

@ -0,0 +1,2 @@
[plan]
schema = { nickel = "schema.ncl" }

View File

@ -0,0 +1,5 @@
{
ProjectSchema = {
name | String
}
}

4
crates/cuddle/src/lib.rs Normal file
View File

@ -0,0 +1,4 @@
mod plan;
mod project;
mod schema_validator;
mod state;

View File

@ -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<ProjectPlan>,
plan: Option<Plan>,
plan: Option<ClonedPlan>,
}
struct Cuddle<S = Start> {
@ -54,13 +59,32 @@ impl Cuddle<Start> {
impl Cuddle<PrepareProject> {
pub async fn prepare_plan(&self) -> anyhow::Result<Cuddle<PreparePlan>> {
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
};
Ok(Cuddle {
state: PreparePlan {
project: self.state.project.clone(),
plan,
},
})
}
}
todo!()
impl Cuddle<PreparePlan> {
pub async fn build_state(&self) -> anyhow::Result<Cuddle<ValidatedState>> {
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 })
}
}

View File

@ -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<Self> {
let config: RawPlanConfig = toml::from_str(content)?;
Ok(Self::new(config, root))
}
pub async fn from_path(path: &Path) -> anyhow::Result<Self> {
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<RawPlanSchema>,
}
#[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(())
}
}

View File

@ -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<Self> {
let config: RawConfig = toml::from_str(content)?;
Ok(Self::new(config, root))
}
pub async fn from_path(path: &Path) -> anyhow::Result<Self> {
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<Self> {
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<Option<Self>> {
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<PlanConfig>,
}
@ -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(())
}

View File

@ -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<Option<()>> {
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"),
}
}
}

View File

@ -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<TempDirGuard> {
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(())
}
}

View File

@ -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<ClonedPlan>,
) -> anyhow::Result<RawState> {
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<ValidatedState> {
// 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<plan::RawPlan>,
}
pub struct ValidatedState {}