Ensure project name starts with uppercase (#3947)

Fixes the regression when IDE fails to create a project from template. Project name should start with an upper case letter to pass the server side validation.
This commit is contained in:
Dmitry Bushev 2022-12-06 20:43:33 +03:00 committed by GitHub
parent f8834f5a63
commit 20fd9f4b96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 133 additions and 19 deletions

View File

@ -28,6 +28,103 @@ pub const STANDARD_BASE_LIBRARY_PATH: &str = concatcp!(STANDARD_NAMESPACE, ".",
// ================
// === Template ===
// ================
#[allow(missing_docs)]
#[derive(Copy, Clone, Debug, Fail)]
pub enum InvalidTemplateName {
#[fail(display = "The template name contains invalid characters.")]
ContainsInvalidCharacters,
}
/// The project template name.
#[derive(Clone, Debug)]
pub struct Template {
name: String,
}
impl Template {
/// Create the project template from string.
///
/// # Example
///
/// ```rust
/// # use double_representation::name::project::Template;
/// assert!(Template::from_text("hello").is_ok());
/// assert!(Template::from_text("hello_world").is_err());
/// ```
pub fn from_text(text: impl AsRef<str>) -> FallibleResult<Self> {
if text.as_ref().contains(|c: char| !c.is_ascii_alphanumeric()) {
Err(InvalidTemplateName::ContainsInvalidCharacters.into())
} else {
Ok(Template { name: text.as_ref().to_owned() })
}
}
/// Create the project template from string without validation.
pub fn unsafe_from_text(text: impl AsRef<str>) -> Self {
Template { name: text.as_ref().to_owned() }
}
/// Create a project name from the template name.
/// # Example
///
/// ```rust
/// # use double_representation::name::project::Template;
/// let template = Template::unsafe_from_text("hello");
/// assert_eq!(template.to_project_name(), "Hello".to_owned());
/// ```
pub fn to_project_name(&self) -> String {
let mut name = self.name.to_string();
// Capitalize
if let Some(r) = name.get_mut(0..1) {
r.make_ascii_uppercase();
}
name
}
}
// === Conversions From and Into String ===
impl TryFrom<&str> for Template {
type Error = failure::Error;
fn try_from(text: &str) -> Result<Self, Self::Error> {
Self::from_text(text)
}
}
impl TryFrom<String> for Template {
type Error = failure::Error;
fn try_from(text: String) -> Result<Self, Self::Error> {
Self::from_text(text)
}
}
impl From<Template> for String {
fn from(template: Template) -> Self {
String::from(&template.name)
}
}
impl From<&Template> for String {
fn from(template: &Template) -> Self {
template.name.to_owned()
}
}
impl Display for Template {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)
}
}
// =====================
// === QualifiedName ===
// =====================

View File

@ -7,6 +7,7 @@ use crate::prelude::*;
use crate::notification;
use double_representation::name::project;
use mockall::automock;
use parser_scala::Parser;
@ -134,7 +135,7 @@ pub trait ManagingProjectAPI {
///
/// `template` is an optional project template name. Available template names are defined in
/// `lib/scala/pkg/src/main/scala/org/enso/pkg/Template.scala`.
fn create_new_project(&self, template: Option<String>) -> BoxFuture<FallibleResult>;
fn create_new_project(&self, template: Option<project::Template>) -> BoxFuture<FallibleResult>;
/// Return a list of existing projects.
fn list_projects(&self) -> BoxFuture<FallibleResult<Vec<ProjectMetadata>>>;

View File

@ -11,6 +11,7 @@ use crate::controller::ide::API;
use crate::ide::initializer;
use crate::notification;
use double_representation::name::project;
use engine_protocol::project_manager;
use engine_protocol::project_manager::MissingComponentAction;
use engine_protocol::project_manager::ProjectMetadata;
@ -119,22 +120,24 @@ impl API for Handle {
impl ManagingProjectAPI for Handle {
#[profile(Objective)]
fn create_new_project(&self, template: Option<String>) -> BoxFuture<FallibleResult> {
fn create_new_project(&self, template: Option<project::Template>) -> BoxFuture<FallibleResult> {
async move {
use model::project::Synchronized as Project;
let list = self.project_manager.list_projects(&None).await?;
let existing_names: HashSet<_> =
list.projects.into_iter().map(|p| p.name.into()).collect();
let name = template.clone().unwrap_or_else(|| UNNAMED_PROJECT_NAME.to_owned());
let name = choose_new_project_name(&existing_names, &name);
let name = make_project_name(&template);
let name = choose_unique_project_name(&existing_names, &name);
let name = ProjectName::new_unchecked(name);
let version =
enso_config::ARGS.preferred_engine_version.as_ref().map(ToString::to_string);
let action = MissingComponentAction::Install;
let create_result =
self.project_manager.create_project(&name, &template, &version, &action).await?;
let create_result = self
.project_manager
.create_project(&name, &template.map(|t| t.into()), &version, &action)
.await?;
let new_project_id = create_result.project_id;
let project_mgr = self.project_manager.clone_ref();
let new_project = Project::new_opened(&self.logger, project_mgr, new_project_id);
@ -167,7 +170,7 @@ impl ManagingProjectAPI for Handle {
/// Select a new name for the project in a form of <suggested_name>_N, where N is a unique sequence
/// number.
fn choose_new_project_name(existing_names: &HashSet<String>, suggested_name: &str) -> String {
fn choose_unique_project_name(existing_names: &HashSet<String>, suggested_name: &str) -> String {
let first_candidate = suggested_name.to_owned();
let nth_project_name = |i| iformat!("{suggested_name}_{i}");
let candidates = (1..).map(nth_project_name);
@ -175,3 +178,11 @@ fn choose_new_project_name(existing_names: &HashSet<String>, suggested_name: &st
// The iterator have no end, so we can safely unwrap.
candidates.find(|c| !existing_names.contains(c)).unwrap()
}
/// Come up with a project name.
fn make_project_name(template: &Option<project::Template>) -> String {
template
.as_ref()
.map(|t| t.to_project_name())
.unwrap_or_else(|| UNNAMED_PROJECT_NAME.to_owned())
}

View File

@ -103,20 +103,25 @@ impl Model {
#[profile(Task)]
fn create_project(&self, template: Option<&str>) {
let controller = self.controller.clone_ref();
let template = template.map(ToOwned::to_owned);
crate::executor::global::spawn(async move {
if let Ok(managing_api) = controller.manage_projects() {
if let Err(err) = managing_api.create_new_project(template.clone()).await {
if let Some(template) = template {
error!("Could not create new project from template {template}: {err}.");
} else {
error!("Could not create new project: {err}.");
if let Ok(template) =
template.map(double_representation::name::project::Template::from_text).transpose()
{
crate::executor::global::spawn(async move {
if let Ok(managing_api) = controller.manage_projects() {
if let Err(err) = managing_api.create_new_project(template.clone()).await {
if let Some(template) = template {
error!("Could not create new project from template {template}: {err}.");
} else {
error!("Could not create new project: {err}.");
}
}
} else {
warn!("Project creation failed: no ProjectManagingAPI available.");
}
} else {
warn!("Project creation failed: no ProjectManagingAPI available.");
}
});
})
} else if let Some(template) = template {
error!("Invalid project template name: {template}");
};
}
}