Files
windmill/backend/windmill-common/src/workspace_dependencies.rs
T

1245 lines
38 KiB
Rust

use itertools::Itertools;
use regex::Regex;
use serde::{Deserialize, Serialize};
use sqlx::PgExecutor;
use crate::{
cache::workspace_dependencies::{
fetch_workspace_dependencies, get_cached_is_unnamed_workspace_dependencies_exists,
set_cached_is_unnamed_workspace_dependencies_exists,
},
error,
min_version::MIN_VERSION_SUPPORTS_V0_WORKSPACE_DEPENDENCIES,
scripts::ScriptLang,
utils::calculate_hash,
worker::Connection,
};
use phf::phf_set;
pub static BLACKLIST: phf::Set<&'static str> = phf_set! {
"u/admin/hub_sync",
};
lazy_static::lazy_static! {
static ref WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: bool = std::env::var("WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES").is_ok();
}
/// Minimum Windmill version required for workspace dependencies feature
pub const MIN_VERSION_WORKSPACE_DEPENDENCIES: &str = "1.587.0";
pub async fn min_version_supports_v0_workspace_dependencies() -> error::Result<()> {
// Check if workers support workspace dependencies feature
if !*WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES
&& !MIN_VERSION_SUPPORTS_V0_WORKSPACE_DEPENDENCIES.met().await
{
tracing::warn!(
"Workspace dependencies feature will be disabled because not all workers support it (minimum version {} required)",
MIN_VERSION_WORKSPACE_DEPENDENCIES
);
return Err(error::Error::WorkersAreBehind {
feature: "Workspace dependencies".to_string(),
min_version: MIN_VERSION_WORKSPACE_DEPENDENCIES.to_string(),
});
} else {
Ok(())
}
}
pub type RawWorkspaceDependencies = std::collections::HashMap<String, String>;
/// Removes workspace dependencies annotation comments from lock files.
/// This is used when passing locks to resolver expect no comments (looking at you, json).
/// IMPORTANT: lock is expected to start with annotations
pub fn clean_lock_from_annotations(lock: &str, language: ScriptLang) -> String {
let mat = format!("{} workspace-dependencies", language.as_comment_lit());
lock.lines().filter(|l| !l.starts_with(&mat)).collect()
}
pub fn get_raw_workspace_dependencies(
raw_workspace_dependencies_o: &Option<RawWorkspaceDependencies>,
name: Option<String>,
language: ScriptLang,
workspace_id: String,
) -> Option<WorkspaceDependencies> {
raw_workspace_dependencies_o
.as_ref()
.zip(WorkspaceDependencies::to_path(&name, language).ok())
.and_then(|(hm, path)| hm.get(&path))
.map(|raw_content| WorkspaceDependencies {
name,
workspace_id,
created_at: chrono::Utc::now(),
language,
content: raw_content.to_owned(),
..Default::default()
})
}
fn map_err(e: String) -> error::Error {
error::Error::FeatureUnavailable(e)
}
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
pub struct WorkspaceDependencies {
/// Global id (across all workspaces)
id: i64,
archived: bool,
/// If not set becomes default for given language
pub name: Option<String>,
pub description: Option<String>,
pub workspace_id: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub language: ScriptLang,
pub content: String,
}
impl WorkspaceDependencies {
pub fn hash(&self) -> String {
calculate_hash(&self.content)
}
/// Marks workspace dependencies as archived.
pub async fn archive<'c>(
name: Option<String>,
language: ScriptLang,
workspace_id: &str,
e: impl PgExecutor<'c>,
) -> error::Result<()> {
if language.as_dependencies_filename().is_none() {
return Ok(());
}
sqlx::query!(
"
UPDATE workspace_dependencies
SET archived = true
WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND archived = false AND language = $3
",
name,
workspace_id,
language as ScriptLang
)
.execute(e)
.await?;
Ok(())
}
/// Permanently deletes workspace dependencies from the database.
pub async fn delete<'c>(
name: Option<String>,
language: ScriptLang,
workspace_id: &str,
e: impl PgExecutor<'c>,
) -> error::Result<()> {
if language.as_dependencies_filename().is_none() {
return Ok(());
}
sqlx::query!(
"
DELETE
FROM workspace_dependencies
WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND language = $3
",
name,
workspace_id,
language as ScriptLang
)
.execute(e)
.await?;
Ok(())
}
/// Lists all active workspace dependencies for a workspace.
pub async fn list<'c>(workspace_id: &str, e: impl PgExecutor<'c>) -> error::Result<Vec<Self>> {
sqlx::query_as!(
Self,
r##"
SELECT id, created_at, archived, name, description, workspace_id, content, language AS "language: ScriptLang"
FROM workspace_dependencies
WHERE archived = false AND workspace_id = $1
"##,
workspace_id,
)
.fetch_all(e)
.await
.map_err(error::Error::from)
}
async fn get_latest_id<'c>(
name: Option<String>,
language: ScriptLang,
workspace_id: &str,
e: impl PgExecutor<'c>,
) -> error::Result<Option<i64>> {
tracing::debug!(
workspace_id = %workspace_id,
?language,
?name,
"fetching latest workspace dependencies id"
);
// Bunnative and Nativets workspace dependencies go under Bun language
let language = match language {
ScriptLang::Nativets | ScriptLang::Bunnative => ScriptLang::Bun,
l => l,
};
let result = sqlx::query_scalar!(
r#"
SELECT id FROM workspace_dependencies
WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND archived = false AND language = $3
LIMIT 1
"#,
name,
workspace_id,
language as ScriptLang
)
.fetch_optional(e)
.await
.map_err(error::Error::from)?;
tracing::debug!(
workspace_id = %workspace_id,
?language,
?name,
?result,
"fetched latest workspace dependencies id"
);
Ok(result)
}
/// Gets the latest version of workspace dependencies by name and language.
pub async fn get_latest(
name: Option<String>,
language: ScriptLang,
workspace_id: &str,
conn: Connection,
) -> error::Result<Option<Self>> {
let Some(dependencies_filename) = language.as_dependencies_filename() else {
return Ok(None);
};
if name.is_none()
&& get_cached_is_unnamed_workspace_dependencies_exists(
dependencies_filename.clone(),
workspace_id.to_owned(),
)
.map(|exists| exists == false)
.unwrap_or_default()
{
tracing::debug!(
workspace_id = %workspace_id,
?language,
"skipping unnamed workspace dependencies fetch - cached as non-existent"
);
return Ok(None);
}
// Fetch from database or HTTP
let wd = match &conn {
Connection::Sql(db) => {
let Some(id) =
Self::get_latest_id(name.clone(), language, workspace_id, db).await?
else {
if name.is_none() {
set_cached_is_unnamed_workspace_dependencies_exists(
dependencies_filename.clone(),
workspace_id.to_owned(),
false,
);
}
tracing::debug!(
workspace_id = %workspace_id,
?language,
?name,
"no latest workspace dependencies found"
);
return Ok(None);
};
tracing::debug!(
workspace_id = %workspace_id,
?language,
?name,
id,
"fetching workspace dependencies by id from cache or db"
);
Some(fetch_workspace_dependencies(id, workspace_id.to_owned(), db).await?)
}
Connection::Http(http_client) => http_client
.get::<Option<WorkspaceDependencies>>(&format!(
"/api/w/{workspace_id}/agent_workers/workspace_dependencies/get_latest/{}{}",
language.as_str(),
if let Some(ref name_val) = name {
format!("?name={name_val}")
} else {
"".to_owned()
}
))
.await
.map_err(error::Error::from)?,
};
if name.is_none() {
set_cached_is_unnamed_workspace_dependencies_exists(
dependencies_filename,
workspace_id.to_owned(),
wd.is_some(),
);
}
Ok(wd)
}
/// Gets workspace dependencies by their unique ID.
pub async fn get<'c>(
id: i64,
workspace_id: String,
e: impl PgExecutor<'c>,
) -> error::Result<Self> {
sqlx::query_as!(
Self,
r#"
SELECT id, content, language AS "language: ScriptLang", name, archived, description, workspace_id, created_at
FROM workspace_dependencies
WHERE id = $1 AND workspace_id = $2
LIMIT 1
"#,
id,
&workspace_id
)
.fetch_one(e)
.await
.map_err(error::Error::from)
}
/// Gets the version history for workspace dependencies.
pub async fn get_history<'c>(
name: Option<String>,
language: ScriptLang,
workspace_id: &str,
e: impl PgExecutor<'c>,
) -> error::Result<Vec<i64>> {
if language.as_dependencies_filename().is_none() {
return Ok(vec![]);
}
sqlx::query_scalar!(
r#"
SELECT id FROM workspace_dependencies
WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND language = $3
"#,
name,
workspace_id,
language as ScriptLang
)
.fetch_all(e)
.await
.map_err(error::Error::from)
}
pub fn to_path(name: &Option<String>, language: ScriptLang) -> error::Result<String> {
let requirements_filename =
language
.as_dependencies_filename()
.ok_or(error::Error::BadConfig(format!(
"workspace dependencies are not supported for: {}",
language.as_str()
)))?;
Ok(if let Some(name) = name {
format!("dependencies/{name}.{requirements_filename}")
} else {
format!("dependencies/{requirements_filename}")
})
}
}
#[derive(Debug, Clone)]
pub struct WorkspaceDependenciesPrefetched {
language: ScriptLang,
runnable_path: String,
#[allow(dead_code)]
workspace_id: String,
internal: WorkspaceDependenciesPrefetchedInternal,
}
#[derive(Debug, Clone)]
enum WorkspaceDependenciesPrefetchedInternal {
Explicit(WorkspaceDependenciesAnnotatedRefs<WorkspaceDependencies>),
Implicit { workspace_dependencies: WorkspaceDependencies, mode: Mode },
None,
}
impl WorkspaceDependenciesPrefetched {
pub async fn extract<'c>(
code: &str,
language: ScriptLang,
workspace_id: &str,
raw_workspace_dependencies_o: &Option<RawWorkspaceDependencies>,
runnable_path: &str,
conn: Connection,
) -> error::Result<WorkspaceDependenciesPrefetched> {
use WorkspaceDependenciesPrefetchedInternal::*;
tracing::debug!(workspace_id, ?language, "extracting workspace dependencies");
Box::pin(async {
let r = if let Some(wdar) =
crate::scripts::extract_workspace_dependencies_annotated_refs(&language, code, runnable_path)
{
tracing::debug!(workspace_id, ?language, "found explicit annotations");
let expanded = wdar
.expand(language, workspace_id, raw_workspace_dependencies_o, conn)
.await?;
Explicit(expanded)
// First try in raw dependencies
} else if let Some(workspace_dependencies) = get_raw_workspace_dependencies(
raw_workspace_dependencies_o,
Option::None,
language,
workspace_id.to_owned(),
) {
tracing::debug!(
workspace_id,
?language,
dep_id = workspace_dependencies.id,
"using implicit raw"
);
// Hardcode to manual for now.
Implicit { workspace_dependencies, mode: Mode::manual }
} else if let Some(workspace_dependencies) =
// If not found, fetch from db
WorkspaceDependencies::get_latest(
Option::None,
language,
workspace_id,
conn,
)
.await?
{
tracing::debug!(
workspace_id,
?language,
dep_id = workspace_dependencies.id,
"using implicit default"
);
// Hardcode to manual for now.
Implicit { workspace_dependencies, mode: Mode::manual }
} else {
tracing::debug!(workspace_id, ?language, "no dependencies found");
None
};
// Crucial part. It will drop all blacklisted runnables
WorkspaceDependenciesPrefetched {
internal: r,
language,
runnable_path: runnable_path.to_owned(),
workspace_id: workspace_id.to_owned(),
}
.preprocess()
.await
})
.await
}
pub fn get_python(&self) -> error::Result<Option<String>> {
use WorkspaceDependenciesPrefetchedInternal::*;
Ok(match &self.internal {
Explicit(wdar @ WorkspaceDependenciesAnnotatedRefs { inline, external, .. }) => {
tracing::debug!(
"Processing explicit workspace dependencies with inline: {}, external count: {}",
inline.is_some(),
external.len()
);
wdar.assert_inline_or_external_or_none().map_err(map_err)?;
wdar.assert_external_less_than(2).map_err(map_err)?;
wdar.assert_no_extra_mode_for_external().map_err(map_err)?;
external
.get(0)
.map(|wd| wd.content.clone())
.or(inline.to_owned())
}
Implicit { workspace_dependencies, .. } => {
self.internal.assert_no_extra_mode().map_err(map_err)?;
Some(workspace_dependencies.content.to_owned())
}
None => Option::None,
})
}
pub fn get_bun(&self) -> error::Result<Option<String>> {
use WorkspaceDependenciesPrefetchedInternal::*;
self.internal.assert_no_extra_mode().map_err(map_err)?;
Ok(match &self.internal {
Explicit(wdar @ WorkspaceDependenciesAnnotatedRefs { external, .. }) => {
wdar.assert_no_inline().map_err(map_err)?;
wdar.assert_external_less_than(2).map_err(map_err)?;
external
.get(0)
.map(|wd| wd.content.clone())
.or(Some("".to_owned()))
}
Implicit { workspace_dependencies, .. } => Some(workspace_dependencies.content.clone()),
None => Option::None,
})
}
pub fn get_go(&self) -> error::Result<Option<String>> {
// NOTE: go is disabled for now:
// https://discord.com/channels/930051556043276338/1031563866641018910/1443541229349634189
self.internal
.assert_no_workspace_dependencies()
.map_err(map_err)?;
Ok(None)
// use WorkspaceDependenciesPrefetchedInternal::*;
// self.internal.assert_no_manual_mode().map_err(map_err)?;
// Ok(match &self.internal {
// Explicit(wdar @ WorkspaceDependenciesAnnotatedRefs { external, .. }) => {
// wdar.assert_no_inline().map_err(map_err)?;
// wdar.assert_external_less_than(2).map_err(map_err)?;
// external.get(0).map(|wd| dbg!(wd.content.clone())).or(Some(
// "
// module mymod
// go 1.25
// require ()
// "
// .to_owned(),
// ))
// }
// Implicit { workspace_dependencies, .. } => Some(workspace_dependencies.content.clone()),
// None => Option::None,
// }
// .map(|go_mod_content| {
// if let Some(module) = go_mod_content
// .lines()
// .find(|l| l.trim_start().starts_with("module "))
// {
// go_mod_content.replace(module, "module mymod")
// } else {
// format!("module mymod\n{go_mod_content}")
// }
// }))
}
pub fn get_php(&self) -> error::Result<Option<String>> {
use WorkspaceDependenciesPrefetchedInternal::*;
self.internal.assert_no_extra_mode().map_err(map_err)?;
Ok(match &self.internal {
Explicit(wdar @ WorkspaceDependenciesAnnotatedRefs { external, .. }) => {
wdar.assert_no_inline().map_err(map_err)?;
wdar.assert_external_less_than(2).map_err(map_err)?;
external
.get(0)
.map(|wd| wd.content.clone())
.or(Some(r#"{"require": {}}"#.to_owned()))
}
Implicit { workspace_dependencies, .. } => Some(workspace_dependencies.content.clone()),
None => Option::None,
})
}
/// Is the runnable permitted to have external references
pub fn is_external_references_permitted(runnable_path: &str) -> bool {
!BLACKLIST.contains(runnable_path) && !runnable_path.starts_with("hub/")
}
async fn preprocess(mut self) -> error::Result<WorkspaceDependenciesPrefetched> {
// NOTE: we should error if it is not compatible. User should either update workers or do not use incompatible feature.
// Check if compatible with legacy and if not check that all workers run compatible versions.
if let Err(feature) = self.check_legacy_compat() {
min_version_supports_v0_workspace_dependencies()
.await
.map_err(|_| {
// NOTE: this error is flakey, sometimes it will error, somethimes it will just ignore.
error::Error::WorkersAreBehind {
feature,
min_version: MIN_VERSION_WORKSPACE_DEPENDENCIES.to_owned(),
}
})?;
}
// NOTE: if you update or add new language, add compatibility checks here.
// for example if you were to update lang do:
// if let Err(incompatible_e) = self.check_v0_python_compat() {
// min_version_supports_v1_workspace_dependencies_python()
// ...
// }
//
// Where `check_v0_python_compat` describes all features of previous python
if !Self::is_external_references_permitted(&self.runnable_path) {
self.remove_external_references();
}
Ok(self)
}
fn check_legacy_compat(&self) -> Result<(), String> {
use ScriptLang::*;
use WorkspaceDependenciesPrefetchedInternal::*;
match (self.language, &self.internal) {
// These languages except for python had none of this functionality
(Php | Bun | Bunnative | Go, wdp) => wdp.assert_no_workspace_dependencies()?,
// Python, had #(extra_)requirements:
// but it had no external requirements.
// that's why we check if it is using only inline syntax
(Python3, Explicit(wdar)) => wdar.assert_no_external()?,
(Python3, wdp) => wdp.assert_no_implicit()?,
(lang @ _, _) => {
tracing::warn!(
self.runnable_path,
"skipping workspace dependencies for unsupported language {}",
lang.as_str()
);
return Ok(());
}
}
Ok(())
}
// TODO:
// pub fn check_v0_compat(&self) -> bool {}
// pub fn check_v1_compat(&self) -> bool {}
// pub fn check_v1_python_compat
fn remove_external_references(&mut self) {
use WorkspaceDependenciesPrefetchedInternal::*;
match self.internal {
Explicit(WorkspaceDependenciesAnnotatedRefs { ref mut external, .. })
if !external.is_empty() =>
{
external.clear();
}
// Implicit is an external reference to the default. So we just replace it to none
ref mut wdp @ Implicit { .. } => drop(std::mem::replace(wdp, None)),
// Return early not to show warning
_ => return,
}
tracing::warn!(
self.runnable_path,
"skipping external workspace dependencies for runnable"
);
}
pub async fn to_lock_header(&self) -> Option<String> {
use WorkspaceDependenciesPrefetchedInternal::*;
if min_version_supports_v0_workspace_dependencies()
.await
.is_err()
{
return Option::None;
}
let mut header = vec![];
let prepend_mode = |mode| {
format!(
"{} workspace-dependencies-mode: {}",
self.language.as_comment_lit(),
mode
)
};
let insert_line = |hash, name: Option<String>| {
format!(
"{} workspace-dependencies: {}:{}",
self.language.as_comment_lit(),
name.unwrap_or("default".to_owned()),
hash
)
};
match &self.internal {
Explicit(workspace_dependencies_annotated_refs) => {
header.push(prepend_mode(workspace_dependencies_annotated_refs.mode));
for wd in &workspace_dependencies_annotated_refs.external {
header.push(insert_line(wd.hash(), wd.name.clone()));
}
}
Implicit { workspace_dependencies: wd, mode } => {
header.push(prepend_mode(*mode));
header.push(insert_line(wd.hash(), Option::None));
}
None => return Option::None,
}
Some(header.join("\n"))
}
pub fn is_manual(&self) -> bool {
self.internal.get_mode() == Some(Mode::manual)
}
}
#[derive(Debug, Clone)]
pub struct WorkspaceDependenciesAnnotatedRefs<T> {
/// ```python
/// # requirements:
/// # rich==x.y.z <<
/// # pandas==x.y.z <<
/// ```
pub inline: Option<String>,
/// ```python
/// # requirements: default, base, prod
/// ^^^^^^^ ^^^^ ^^^^
/// ```
///
/// Can either be a [[String]] or [[WorkspaceDependencies]]
///
/// The workflow is following:
/// 1. You create Self with <[[String]]> - this will fetch a minimal amount of info (just the name).
/// 2. You [[Self::expand]] to replace all external names with <[[WorkspaceDependencies]]>
pub external: Vec<T>,
pub mode: Mode,
}
/// `# extra_requirements:` - Extra
/// `# requirements:` - Manual
#[allow(non_camel_case_types)]
#[derive(PartialEq, Eq, strum_macros::Display, Clone, Copy, Debug)]
pub enum Mode {
manual,
extra,
}
impl WorkspaceDependenciesPrefetchedInternal {
fn get_mode(&self) -> Option<Mode> {
use WorkspaceDependenciesPrefetchedInternal::*;
match &self {
Explicit(WorkspaceDependenciesAnnotatedRefs { mode, .. }) | Implicit { mode, .. } => {
Some(*mode)
}
None => Option::None,
}
}
fn assert_no_implicit(&self) -> Result<(), String> {
if matches!(self, Self::Implicit { .. }) {
Err(format!("'default workspace dependencies'"))
} else {
Ok(())
}
}
fn assert_no_workspace_dependencies(&self) -> Result<(), String> {
if !matches!(self, Self::None { .. }) {
Err(format!("'workspace dependencies'"))
} else {
Ok(())
}
}
#[allow(dead_code)]
fn assert_no_explicit(&self) -> Result<(), String> {
if matches!(self, Self::Explicit { .. }) {
Err(format!("'external workspace dependencies'"))
} else {
Ok(())
}
}
fn assert_no_extra_mode(&self) -> Result<(), String> {
if self.get_mode() == Some(Mode::extra) {
Err(format!("'workspace dependencies in extra mode'"))
} else {
Ok(())
}
}
#[allow(dead_code)]
fn assert_no_manual_mode(&self) -> Result<(), String> {
if self.get_mode() == Some(Mode::manual) {
Err(format!("'workspace dependencies in manual mode'"))
} else {
Ok(())
}
}
}
impl WorkspaceDependenciesAnnotatedRefs<WorkspaceDependencies> {
fn assert_no_inline(&self) -> Result<(), String> {
if self.inline.is_none() {
Ok(())
} else {
Err(format!("'inline workspace dependencies'"))
}
}
fn assert_no_extra_mode_for_external(&self) -> Result<(), String> {
if self.mode == Mode::extra && !self.external.is_empty() {
Err(format!("'external workspace dependencies in extra mode'"))
} else {
Ok(())
}
}
#[allow(dead_code)]
fn assert_no_extra_mode_for_inline(&self) -> Result<(), String> {
if self.mode == Mode::extra && self.inline.is_some() {
Err(format!("'inline workspace dependencies in extra mode'"))
} else {
Ok(())
}
}
fn assert_no_external(&self) -> Result<(), String> {
self.assert_external_less_than(1)
.map_err(|_e| format!("'external workspace dependencies'"))
}
fn assert_inline_or_external_or_none(&self) -> Result<(), String> {
if self.inline.is_some() && !self.external.is_empty() {
Err(format!(
"'inline and externally referenced workspace dependencies at the same time'"
))
} else {
Ok(())
}
}
fn assert_external_less_than(&self, amount: usize) -> Result<(), String> {
if self.external.len() < amount {
Ok(())
} else {
Err(format!(
"'multiple external workspace dependencies referenced'",
))
}
}
}
impl WorkspaceDependenciesAnnotatedRefs<String> {
pub(super) async fn expand(
self,
language: ScriptLang,
workspace_id: &str,
raw_workspace_dependencies_o: &Option<RawWorkspaceDependencies>,
conn: Connection,
) -> error::Result<WorkspaceDependenciesAnnotatedRefs<WorkspaceDependencies>> {
let mut res = WorkspaceDependenciesAnnotatedRefs {
inline: self.inline,
external: vec![],
mode: self.mode,
};
for name in self.external {
// "default" maps to unnamed workspace dependencies.
let name = if name == "default" { None } else { Some(name) };
// First try in raw dependencies
if let Some(wd) = get_raw_workspace_dependencies(
raw_workspace_dependencies_o,
name.clone(),
language,
workspace_id.to_owned(),
) {
res.external.push(wd);
// If not found, fetch from db
} else if let Some(wd) = WorkspaceDependencies::get_latest(
name.clone(),
language,
workspace_id,
conn.clone(),
)
.await?
{
res.external.push(wd.clone());
} else {
tracing::warn!(
workspace_id,
?language,
dependency_name = name,
"workspace dependencies not found"
);
}
}
Ok(res)
}
// TODO: Maybe implemented by our Annotations macro
// TODO: Add sep config ':' or '='?
pub fn parse(
comment: &str,
keyword: &str,
code: &str,
validity_re_o: Option<&Regex>,
runnable_path: &str,
) -> Option<Self> {
let (extra_deps, manual_deps) = (format!("extra_{keyword}:"), format!("{keyword}:"));
let Some((pos, mat)) = code.lines().find_position(|l| {
l.starts_with(&comment) && (l.contains(&extra_deps) || l.contains(&manual_deps))
}) else {
return None;
};
let mut lines_it = code.lines().skip(pos);
let mode = if mat.contains(&extra_deps) {
Mode::extra
} else {
Mode::manual
};
let external = {
let next_line = lines_it.next();
if !WorkspaceDependenciesPrefetched::is_external_references_permitted(runnable_path) {
tracing::warn!(
runnable_path,
"skipping external workspace dependencies for runnable"
);
Default::default()
// return Err(error::Error::BadConfig(format!(
// "{runnable_path} should not include external Workspace Dependencies"
// )));
} else {
next_line
.map(|s| {
match mode {
Mode::manual => s.replace(&manual_deps, ""),
Mode::extra => s.replace(&extra_deps, ""),
}
.replace(comment, "")
})
.map(|unparsed| {
unparsed
.split(',')
// TODO: do we want to sort it?
.map(str::trim)
.filter(|s| !s.is_empty())
// .map(FromName::from_name)
.map(str::to_owned)
.collect_vec()
})
.unwrap_or_default()
}
};
let inline_deps = lines_it
.map_while(|l| {
match validity_re_o {
Some(re) => re.captures(l).and_then(|c| c.get(1).map(|m| m.as_str())),
None => {
if !l.starts_with(comment) {
None
} else {
// Skip comment
// If it fails (None) iteration is just finished.
l.get(comment.len()..)
}
}
}
})
.join("\n");
let inline = if inline_deps.trim().is_empty() {
None
} else {
Some(inline_deps)
};
Some(WorkspaceDependenciesAnnotatedRefs {
inline, // TODO: Parse
external,
mode,
})
}
}
#[cfg(test)]
mod workspace_dependencies_tests {
use super::*;
#[test]
fn test_parse_annotation_python_requirements_manual_mode() {
let code = r#"
# requirements: default, base
#requests==2.31.0
#pandas>=1.5.0
def main():
pass
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"requirements",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::manual));
assert_eq!(
result.external,
vec!["default".to_owned(), "base".to_owned()]
);
assert_eq!(
result.inline.as_ref().unwrap(),
"requests==2.31.0\npandas>=1.5.0"
);
}
#[test]
fn test_parse_annotation_python_extra_requirements_mode() {
let code = r#"
# extra_requirements: utils
#numpy>=1.24.0
def main():
pass
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"requirements",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::extra));
assert_eq!(result.external, vec!["utils".to_owned()]);
assert_eq!(result.inline.as_ref().unwrap(), "numpy>=1.24.0");
}
#[test]
fn test_parse_annotation_typescript_requirements() {
let code = r#"
// requirements: utils, base
//{
// "dependencies": {
// "axios": "^1.6.0"
// }
//}
export function main() {}
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"//",
"requirements",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::manual));
assert_eq!(result.external, vec!["utils".to_owned(), "base".to_owned()]);
let expected_inline = r#"{
"dependencies": {
"axios": "^1.6.0"
}
}"#;
assert_eq!(result.inline.as_ref().unwrap(), expected_inline);
}
#[test]
fn test_parse_annotation_with_spacing_variations() {
let code = r#"
#requirements: no_space
def main():
pass
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"requirements",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::manual));
assert_eq!(result.external, vec!["no_space".to_owned()]);
assert!(result.inline.is_none());
}
#[test]
fn test_parse_annotation_with_spacing_variations_spaced() {
let code = r#"
# requirements: with_space
def main():
pass
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"requirements",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::manual));
assert_eq!(result.external, vec!["with_space".to_owned()]);
assert!(result.inline.is_none());
}
#[test]
fn test_parse_annotation_go_style() {
let code = r#"
// go_mod: base,
//github.com/gin-gonic/gin v1.9.1
package main
func main() {}
"#;
let result =
WorkspaceDependenciesAnnotatedRefs::<String>::parse("//", "go_mod", code, None, "")
.unwrap();
assert!(matches!(result.mode, Mode::manual));
assert_eq!(result.external, vec!["base".to_owned()]);
assert_eq!(
result.inline.as_ref().unwrap(),
"github.com/gin-gonic/gin v1.9.1"
);
}
#[test]
fn test_parse_annotation_no_inline_deps() {
let code = r#"
# requirements: default
def main():
pass
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"requirements",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::manual));
assert_eq!(result.external, vec!["default".to_owned()]);
assert!(result.inline.is_none());
}
#[test]
fn test_parse_annotation_inline_only() {
let code = r#"
# requirements:
#requests==2.31.0
# pandas>=1.5.0
def main():
pass
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"requirements",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::manual));
assert!(result.external.is_empty());
assert_eq!(
result.inline.as_ref().unwrap(),
"requests==2.31.0\n pandas>=1.5.0"
);
}
#[test]
fn test_parse_annotation_no_match() {
let code = r#"
def main():
pass
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"requirements",
code,
None,
"",
);
assert!(result.is_none());
}
#[test]
fn test_parse_annotation_php_style() {
let code = r#"
<?php
// requirements: composer
//{
// "require": {
// "guzzlehttp/guzzle": "^7.0"
// }
//}
function main() {}
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"//",
"requirements",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::manual));
assert_eq!(result.external, vec!["composer".to_owned()]);
let expected_inline = r#"{
"require": {
"guzzlehttp/guzzle": "^7.0"
}
}"#;
assert_eq!(result.inline.as_ref().unwrap(), expected_inline);
}
#[test]
fn test_parse_annotation_just_requirements_colon() {
let code = r#"
#requirements:
def main():
pass
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"requirements",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::manual));
assert!(result.external.is_empty());
assert!(result.inline.is_none());
}
#[test]
fn test_parse_annotation_blacklisted() {
let code = r#"
#requirements: hello, world
def main():
pass
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"requirements",
code,
None,
"u/admin/hub_sync",
)
.unwrap();
assert!(matches!(result.mode, Mode::manual));
assert!(result.external.is_empty());
assert!(result.inline.is_none());
}
}