Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
parent
c2b7e44ea3
commit
1ba6cf79c0
@ -0,0 +1 @@
|
||||
[plan]
|
@ -1,2 +1,5 @@
|
||||
[plan]
|
||||
path = "../plan"
|
||||
|
||||
[project]
|
||||
name = "basic"
|
||||
|
2
crates/cuddle/examples/schema/plan/cuddle.plan.toml
Normal file
2
crates/cuddle/examples/schema/plan/cuddle.plan.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[plan]
|
||||
schema = { nickel = "schema.ncl" }
|
0
crates/cuddle/examples/schema/plan/cuddle.toml
Normal file
0
crates/cuddle/examples/schema/plan/cuddle.toml
Normal file
5
crates/cuddle/examples/schema/plan/schema.ncl
Normal file
5
crates/cuddle/examples/schema/plan/schema.ncl
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
ProjectSchema = {
|
||||
name | String
|
||||
}
|
||||
}
|
4
crates/cuddle/src/lib.rs
Normal file
4
crates/cuddle/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod plan;
|
||||
mod project;
|
||||
mod schema_validator;
|
||||
mod state;
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
30
crates/cuddle/src/schema_validator.rs
Normal file
30
crates/cuddle/src/schema_validator.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
}
|
111
crates/cuddle/src/schema_validator/nickel.rs
Normal file
111
crates/cuddle/src/schema_validator/nickel.rs
Normal 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(())
|
||||
}
|
||||
}
|
45
crates/cuddle/src/state.rs
Normal file
45
crates/cuddle/src/state.rs
Normal 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 {}
|
Loading…
Reference in New Issue
Block a user