use std::{path::Path, sync::Arc};
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
use crate::ir::{Address, AttributeMap, Component, Expression, ModuleId, ProviderRef, Span};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase", tag = "kind", content = "value")]
pub enum ModuleSource {
Local(Arc<str>),
Registry(Arc<str>),
Git(Arc<str>),
External(Arc<str>),
}
impl ModuleSource {
#[must_use]
pub fn classify(raw: &str) -> Self {
let raw_arc: Arc<str> = Arc::from(raw);
if raw.starts_with("./") || raw.starts_with("../") {
Self::Local(raw_arc)
} else if raw.starts_with("git::")
|| raw.starts_with("git@")
|| raw.contains(".git")
|| raw.starts_with("github.com/")
{
Self::Git(raw_arc)
} else if raw.contains('/') && !raw.starts_with('/') && raw.matches('/').count() >= 2 {
Self::Registry(raw_arc)
} else {
Self::External(raw_arc)
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
#[builder(field_defaults(setter(into)))]
pub struct ModuleCall {
pub address: Address,
pub source_raw: Arc<str>,
pub source: ModuleSource,
#[builder(default)]
pub resolved: Option<ModuleId>,
#[builder(default)]
pub providers: Vec<(Arc<str>, ProviderRef)>,
#[builder(default)]
pub inputs: AttributeMap,
#[builder(default)]
pub count_expr: Option<Expression>,
#[builder(default)]
pub for_each_expr: Option<Expression>,
pub span: Span,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
#[builder(field_defaults(setter(into)))]
pub struct Module {
pub id: ModuleId,
pub source: ModuleSource,
#[builder(default)]
#[serde(with = "crate::ir::path_serde::arc_path_opt")]
pub canonical_path: Option<Arc<Path>>,
pub component: Component,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_should_classify_local_source() {
assert!(matches!(
ModuleSource::classify("./foo"),
ModuleSource::Local(_)
));
assert!(matches!(
ModuleSource::classify("../../modules/rds"),
ModuleSource::Local(_)
));
}
#[test]
fn test_should_classify_git_source() {
for s in [
"git::https://github.com/x/y.git",
"github.com/x/y",
"git@github.com:x/y.git",
] {
assert!(
matches!(ModuleSource::classify(s), ModuleSource::Git(_)),
"expected Git classification for {s}"
);
}
}
#[test]
fn test_should_classify_registry_source() {
assert!(matches!(
ModuleSource::classify("terraform-aws-modules/eks/aws"),
ModuleSource::Registry(_)
));
}
#[test]
fn test_should_round_trip_module_source_via_serde() {
let s = ModuleSource::Local(Arc::<str>::from("./foo"));
let json = serde_json::to_string(&s).unwrap();
let back: ModuleSource = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
#[test]
fn test_should_classify_absolute_unix_path_as_external() {
assert!(matches!(
ModuleSource::classify("/abs/path"),
ModuleSource::External(_)
));
}
#[test]
fn test_should_classify_bare_name_as_external() {
assert!(matches!(
ModuleSource::classify("just_a_name"),
ModuleSource::External(_)
));
}
#[test]
fn test_should_classify_single_slash_as_external_not_registry() {
assert!(matches!(
ModuleSource::classify("foo/bar"),
ModuleSource::External(_)
));
}
}