use std::path::{Path, PathBuf};
use crate::actions::abstract_action::AbstractAction;
use crate::actions::{ActionInvocation, ActionKind, ActionSpec, action_spec};
use crate::commands::{Input, InputValue};
use crate::configuration::{Configuration, DEFAULT_COLLECTION, GenerateSpec};
use crate::schematics::{CollectionFactory, SchematicOption};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GenerateAction;
impl GenerateAction {
pub const fn new() -> Self {
Self
}
pub fn spec(&self) -> &'static ActionSpec {
action_spec(ActionKind::Generate).expect("generate action spec")
}
pub fn handle_invocation(&self, inputs: Vec<Input>, options: Vec<Input>) -> ActionInvocation {
<Self as AbstractAction>::handle(self, inputs, options, Vec::new())
}
pub fn build_plan(
&self,
inputs: &[Input],
configuration: &Configuration,
options: GenerateActionPlanOptions,
) -> Result<GenerateActionPlan, String> {
build_generate_action_plan(inputs, configuration, options)
}
}
impl AbstractAction for GenerateAction {
fn kind(&self) -> ActionKind {
ActionKind::Generate
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct GenerateActionPlanOptions {
pub selected_project: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GenerateActionPlan {
pub collection: String,
pub schematic: String,
pub schematic_options: Vec<SchematicOption>,
pub schematic_command: String,
pub source_root: PathBuf,
pub spec: bool,
pub flat: bool,
pub spec_file_suffix: String,
pub project_selection: Option<ProjectSelectionPlan>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProjectSelectionPlan {
pub default_project_name: String,
pub projects: Vec<String>,
}
pub fn build_generate_action_plan(
inputs: &[Input],
configuration: &Configuration,
options: GenerateActionPlanOptions,
) -> Result<GenerateActionPlan, String> {
let collection = string_value(find_input(inputs, "collection"))
.filter(|value| !value.is_empty())
.unwrap_or_else(|| {
if configuration.collection.is_empty() {
DEFAULT_COLLECTION.to_string()
} else {
configuration.collection.clone()
}
});
let schematic = string_value(find_input(inputs, "schematic"))
.ok_or_else(|| "Unable to find a schematic for this configuration".to_string())?;
let app_name = string_value(find_input(inputs, "project")).unwrap_or_default();
let spec_value = bool_value(find_input(inputs, "spec")).unwrap_or(true);
let flat_value = bool_value(find_input(inputs, "flat")).unwrap_or(false);
let spec_file_suffix_value =
string_value(find_input(inputs, "specFileSuffix")).unwrap_or_default();
let spec_passed_as_input = find_input(inputs, "spec")
.and_then(|input| input.options.as_ref())
.map(|options| options.passed_as_input);
let mut source_root = if app_name.is_empty() {
configuration.source_root.clone()
} else {
project_source_root(configuration, &app_name)
.unwrap_or_else(|| configuration.source_root.clone())
};
let mut generate_options = effective_generate_options(configuration, &app_name);
let mut spec = should_generate_spec_with_options(
&generate_options,
&schematic,
spec_value,
spec_passed_as_input,
);
let mut flat = should_generate_flat_with_options(&generate_options, flat_value);
let mut spec_file_suffix =
get_spec_file_suffix_with_options(&generate_options, &spec_file_suffix_value);
let project_selection = project_selection_plan(configuration, &schematic, &app_name);
if let Some(selected_project_name) = options.selected_project.as_deref() {
let project = selected_project_name.replace(DEFAULT_LABEL, "");
if project != configuration.source_root {
if let Some(selected_source_root) = project_source_root(configuration, &project) {
source_root = selected_source_root;
}
}
if project_selection
.as_ref()
.is_some_and(|selection| selected_project_name != selection.default_project_name)
{
generate_options = effective_generate_options(configuration, selected_project_name);
spec = should_generate_spec_with_options(
&generate_options,
&schematic,
spec_value,
spec_passed_as_input,
);
flat = should_generate_flat_with_options(&generate_options, flat_value);
spec_file_suffix =
get_spec_file_suffix_with_options(&generate_options, &spec_file_suffix_value);
}
}
if let Some(base_dir) = generate_options
.base_dir
.as_deref()
.filter(|value| !value.is_empty())
{
source_root = join_path_string(&source_root, base_dir);
}
let mut schematic_options = map_schematic_options(inputs)?;
schematic_options.push(SchematicOption::new(
"language",
configuration.language.clone(),
));
schematic_options.push(SchematicOption::new("sourceRoot", source_root.clone()));
schematic_options.push(SchematicOption::new("spec", spec));
schematic_options.push(SchematicOption::new("flat", flat));
schematic_options.push(SchematicOption::new(
"specFileSuffix",
spec_file_suffix.clone(),
));
let schematic_command = CollectionFactory::create(collection.clone())
.execute_command(&schematic, &schematic_options)?;
Ok(GenerateActionPlan {
collection,
schematic,
schematic_options,
schematic_command,
source_root: PathBuf::from(source_root),
spec,
flat,
spec_file_suffix,
project_selection,
})
}
pub fn map_schematic_options(inputs: &[Input]) -> Result<Vec<SchematicOption>, String> {
const EXCLUDED_INPUT_NAMES: &[&str] = &["schematic", "spec", "flat", "specFileSuffix"];
inputs
.iter()
.filter(|input| !EXCLUDED_INPUT_NAMES.contains(&input.name.as_str()))
.filter_map(input_to_schematic_option)
.collect()
}
fn effective_generate_options(
configuration: &Configuration,
app_name: &str,
) -> crate::configuration::GenerateOptions {
let mut options = configuration.generate_options.clone();
if let Some(project_options) = configuration
.projects
.get(app_name)
.and_then(|project| project.generate_options.clone())
{
if project_options.spec.is_some() {
options.spec = project_options.spec;
}
if project_options.flat.is_some() {
options.flat = project_options.flat;
}
if project_options.spec_file_suffix.is_some() {
options.spec_file_suffix = project_options.spec_file_suffix;
}
if project_options.base_dir.is_some() {
options.base_dir = project_options.base_dir;
}
}
options
}
pub fn should_generate_spec(
configuration: &Configuration,
schematic: &str,
app_name: &str,
spec_value: bool,
spec_passed_as_input: Option<bool>,
) -> bool {
let generate_options = effective_generate_options(configuration, app_name);
should_generate_spec_with_options(
&generate_options,
schematic,
spec_value,
spec_passed_as_input,
)
}
fn should_generate_spec_with_options(
generate_options: &crate::configuration::GenerateOptions,
schematic: &str,
spec_value: bool,
spec_passed_as_input: Option<bool>,
) -> bool {
if spec_passed_as_input.unwrap_or(true) {
return spec_value;
}
match &generate_options.spec {
Some(GenerateSpec::Bool(value)) => *value,
Some(GenerateSpec::BySchematic(options)) => {
options.get(schematic).copied().unwrap_or(spec_value)
}
None => spec_value,
}
}
pub fn should_generate_flat(configuration: &Configuration, flat_value: bool) -> bool {
should_generate_flat_with_options(&configuration.generate_options, flat_value)
}
fn should_generate_flat_with_options(
generate_options: &crate::configuration::GenerateOptions,
flat_value: bool,
) -> bool {
if flat_value {
return true;
}
generate_options.flat.unwrap_or(flat_value)
}
pub fn get_spec_file_suffix(configuration: &Configuration, spec_file_suffix_value: &str) -> String {
get_spec_file_suffix_with_options(&configuration.generate_options, spec_file_suffix_value)
}
fn get_spec_file_suffix_with_options(
generate_options: &crate::configuration::GenerateOptions,
spec_file_suffix_value: &str,
) -> String {
if !spec_file_suffix_value.is_empty() {
return spec_file_suffix_value.to_string();
}
generate_options
.spec_file_suffix
.clone()
.unwrap_or_else(|| "spec".to_string())
}
pub fn project_selection_plan(
configuration: &Configuration,
schematic: &str,
app_name: &str,
) -> Option<ProjectSelectionPlan> {
if !should_ask_for_project(schematic, configuration, app_name) {
return None;
}
let default_project_name = default_project_name(configuration);
let projects = move_default_project_to_start(configuration, &default_project_name);
Some(ProjectSelectionPlan {
default_project_name,
projects,
})
}
pub fn should_ask_for_project(
schematic: &str,
configuration: &Configuration,
app_name: &str,
) -> bool {
!matches!(schematic, "app" | "sub-app" | "library" | "lib")
&& !configuration.projects.is_empty()
&& app_name.is_empty()
}
fn default_project_name(configuration: &Configuration) -> String {
configuration
.projects
.iter()
.find(|(_, project)| {
project
.source_root
.as_deref()
.is_some_and(|source_root| source_root == configuration.source_root)
})
.map(|(name, _)| format!("{name}{DEFAULT_LABEL}"))
.unwrap_or_else(|| format!("{}{}", configuration.source_root, DEFAULT_LABEL))
}
fn move_default_project_to_start(
configuration: &Configuration,
default_project_name: &str,
) -> Vec<String> {
let default_project = default_project_name.replace(DEFAULT_LABEL, "");
let mut projects = configuration.projects.keys().cloned().collect::<Vec<_>>();
if configuration.source_root != "src" {
projects.retain(|project| project != &default_project);
}
projects.insert(0, default_project_name.to_string());
projects
}
fn project_source_root(configuration: &Configuration, app_name: &str) -> Option<String> {
configuration
.projects
.get(app_name)
.and_then(|project| project.source_root.clone())
}
fn join_path_string(left: &str, right: &str) -> String {
Path::new(left).join(right).to_string_lossy().into_owned()
}
fn input_to_schematic_option(input: &Input) -> Option<Result<SchematicOption, String>> {
let value = input.value.as_ref()?;
Some(match value {
InputValue::Bool(value) => Ok(SchematicOption::new(&input.name, *value)),
InputValue::String(value) => Ok(SchematicOption::new(&input.name, value.clone())),
InputValue::StringList(_) => Err(format!(
"Input `{}` cannot be mapped to a schematic option because arrays are not supported",
input.name
)),
})
}
fn find_input<'a>(inputs: &'a [Input], name: &str) -> Option<&'a Input> {
inputs.iter().find(|input| input.name == name)
}
fn string_value(input: Option<&Input>) -> Option<String> {
match input?.value.as_ref()? {
InputValue::String(value) => Some(value.clone()),
_ => None,
}
}
fn bool_value(input: Option<&Input>) -> Option<bool> {
match input?.value.as_ref()? {
InputValue::Bool(value) => Some(*value),
_ => None,
}
}
const DEFAULT_LABEL: &str = " [ Default ]";