use super::sort::{Graph, SortError, sort_graph};
pub use achitek_source::Spanned;
use achitek_source::TextRange;
use std::{collections::HashMap, vec};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Blueprint {
pub range: Option<TextRange>,
pub version: Option<Spanned<String>>,
pub name: Option<Spanned<String>>,
pub description: Option<Spanned<String>>,
pub author: Option<Spanned<String>>,
pub min_achitek_version: Option<Spanned<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AchitekFile {
blueprint: Blueprint,
prompts: Vec<Spanned<Prompt>>,
}
impl AchitekFile {
pub fn new(blueprint: Blueprint, prompts: Vec<Spanned<Prompt>>) -> Self {
Self { blueprint, prompts }
}
pub fn blueprint(&self) -> &Blueprint {
&self.blueprint
}
pub fn prompts(&self) -> &[Spanned<Prompt>] {
&self.prompts
}
pub(crate) fn into_valid_unchecked(self) -> ValidAchitekFile {
let blueprint = self.blueprint();
let valid_blueprint = ValidBlueprint {
version: blueprint
.version
.as_ref()
.expect("analysis should reject blueprints without a version")
.value
.clone(),
name: blueprint
.name
.as_ref()
.expect("analysis should reject blueprints without a name")
.value
.clone(),
description: blueprint
.description
.as_ref()
.map(|description| description.value.clone()),
author: blueprint.author.as_ref().map(|author| author.value.clone()),
min_achitek_version: blueprint
.min_achitek_version
.as_ref()
.map(|version| version.value.clone()),
};
let valid_prompts = self
.prompts()
.iter()
.map(|spanned_prompt: &Spanned<Prompt>| {
let prompt = &spanned_prompt.value;
ValidPrompt {
name: prompt.name.clone(),
prompt_type: prompt
.prompt_type
.expect("analysis should reject prompts without a type"),
help: prompt.help.clone(),
choices: prompt.choices.clone(),
default: prompt.default.clone(),
required: prompt.required.unwrap_or(false),
depends_on: prompt.depends_on.clone(),
validation: prompt.validation.clone(),
}
})
.collect::<Vec<_>>();
ValidAchitekFile::new(valid_blueprint, valid_prompts)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Prompt {
pub name: String,
pub prompt_type: Option<PromptType>,
pub help: Option<String>,
pub choices: Vec<Value>,
pub choices_declared: bool,
pub default: Option<Value>,
pub required: Option<bool>,
pub depends_on: Option<Dependency>,
pub validation: Validation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PromptType {
String,
Paragraph,
Bool,
Select,
MultiSelect,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Value {
String(String),
Bool(bool),
Integer(u64),
Identifier(String),
Array(Vec<Value>),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Dependency {
Reference(String),
Comparison {
left: String,
operator: ComparisonOperator,
right: Value,
},
Contains {
receiver: String,
argument: Value,
},
All(Vec<Dependency>),
Any(Vec<Dependency>),
}
impl Dependency {
fn references(&self) -> Vec<&str> {
let mut references = Vec::new();
self.collect_references(&mut references);
references
}
fn collect_references<'a>(&'a self, references: &mut Vec<&'a str>) {
match self {
Self::Reference(name) => references.push(name),
Self::Comparison { left, .. } => references.push(left),
Self::Contains { receiver, .. } => references.push(receiver),
Self::All(dependencies) | Self::Any(dependencies) => {
for dependency in dependencies {
dependency.collect_references(references);
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ComparisonOperator {
Equal,
NotEqual,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Validation {
pub regex: Option<String>,
pub min_length: Option<u64>,
pub max_length: Option<u64>,
pub min_selections: Option<u64>,
pub max_selections: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ValidAchitekFile {
blueprint: ValidBlueprint,
prompts: Vec<ValidPrompt>,
}
impl ValidAchitekFile {
pub fn new(blueprint: ValidBlueprint, prompts: Vec<ValidPrompt>) -> Self {
Self { blueprint, prompts }
}
pub fn blueprint(&self) -> &ValidBlueprint {
&self.blueprint
}
pub fn prompts(&self) -> &[ValidPrompt] {
&self.prompts
}
pub fn prompts_in(&self, order: PromptOrder) -> Result<PromptIter<'_>, SortError<String>> {
match order {
PromptOrder::Source => Ok(PromptIter::Source(self.prompts.iter())),
PromptOrder::Dependency => self.prompts_in_dependency_order(),
}
}
fn prompts_in_dependency_order(&self) -> Result<PromptIter<'_>, SortError<String>> {
let prompt_names = self
.prompts
.iter()
.map(|prompt| prompt.name.clone())
.collect::<Vec<_>>();
let edges = self
.prompts
.iter()
.flat_map(|prompt| {
prompt
.depends_on
.as_ref()
.into_iter()
.flat_map(|dependency| dependency.references())
.map(|reference| (reference.to_owned(), prompt.name.clone()))
})
.collect::<Vec<_>>();
let graph = Graph {
nodes: prompt_names,
edges,
};
let sorted_names = sort_graph(&graph)?;
let prompts_by_name = self
.prompts
.iter()
.map(|prompt| (prompt.name.as_str(), prompt))
.collect::<HashMap<_, _>>();
let prompts = sorted_names
.iter()
.filter_map(|name| prompts_by_name.get(name.as_str()).copied())
.collect::<Vec<_>>();
Ok(PromptIter::Dependency(prompts.into_iter()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PromptOrder {
Source,
Dependency,
}
#[derive(Debug, Clone)]
pub enum PromptIter<'a> {
Source(std::slice::Iter<'a, ValidPrompt>),
Dependency(vec::IntoIter<&'a ValidPrompt>),
}
impl<'a> Iterator for PromptIter<'a> {
type Item = &'a ValidPrompt;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Source(prompts) => prompts.next(),
Self::Dependency(prompts) => prompts.next(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ValidBlueprint {
pub version: String,
pub name: String,
pub description: Option<String>,
pub author: Option<String>,
pub min_achitek_version: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ValidPrompt {
pub name: String,
pub prompt_type: PromptType,
pub help: Option<String>,
pub choices: Vec<Value>,
pub default: Option<Value>,
pub required: bool,
pub depends_on: Option<Dependency>,
pub validation: Validation,
}