use anyhow::{Context, Result};
use std::collections::{BTreeMap, HashMap};
use std::rc::Rc;
#[derive(Clone, Debug, PartialEq, Hash, Ord, PartialOrd, Eq)]
pub enum Type {
Boolean,
Integer,
Number,
String,
Array(Box<Type>),
Object(Box<Type>),
Ref(Ref),
Option(Box<Type>),
DiscriminatedUnion(Vec<Type>),
ConstString(String),
}
#[derive(Debug, PartialEq, Hash, Ord, PartialOrd, Eq)]
pub struct InputProperty {
pub name: String,
pub r#type: Type,
pub description: Option<String>,
}
#[derive(Debug, PartialEq, Hash, Ord, PartialOrd, Eq)]
pub struct OutputProperty {
pub name: String,
pub r#type: Type,
pub description: Option<String>,
}
#[derive(Debug, PartialEq, Hash, Ord, PartialOrd, Eq)]
pub struct GlobalTypeProperty {
pub name: String,
pub r#type: Type,
pub description: Option<String>,
}
#[derive(Debug, PartialEq, PartialOrd)]
pub struct GlobalType {
pub element_id: ElementId,
pub value: GlobalTypeValue,
}
#[derive(Debug, PartialEq, PartialOrd)]
pub enum GlobalTypeValue {
Object(Option<String>, Vec<GlobalTypeProperty>),
StringEnum(Option<String>, Vec<StringEnumElement>),
NumberEnum(Option<String>, Vec<NumberEnumElement>),
IntegerEnum(Option<String>, Vec<IntegerEnumElement>),
}
#[derive(Debug, PartialEq, Hash, Ord, PartialOrd, Eq)]
pub struct StringEnumElement {
pub name: String,
pub value: String,
pub description: Option<String>,
}
#[derive(Debug, PartialEq, PartialOrd)]
pub struct NumberEnumElement {
pub name: String,
pub value: f64,
pub description: Option<String>,
}
#[derive(Debug, PartialEq, PartialOrd)]
pub struct IntegerEnumElement {
pub name: String,
pub value: i64,
pub description: Option<String>,
}
#[derive(Debug, PartialEq, Hash, Ord, PartialOrd, Eq)]
pub struct Resource {
pub element_id: ElementId,
pub description: Option<String>,
pub input_properties: Vec<InputProperty>,
pub output_properties: Vec<OutputProperty>,
}
#[derive(Debug, Default, PartialEq, Hash, Ord, PartialOrd, Eq)]
pub struct Provider {
pub description: Option<String>,
pub input_properties: Vec<InputProperty>,
pub output_properties: Vec<OutputProperty>,
}
#[derive(Debug, PartialEq, Hash, Ord, PartialOrd, Eq)]
pub struct Function {
pub element_id: ElementId,
pub description: Option<String>,
pub input_properties: Vec<InputProperty>,
pub output_properties: Vec<OutputProperty>,
}
#[derive(Debug, PartialEq)]
pub struct Package {
pub name: String,
pub display_name: Option<String>,
pub plugin_download_url: Option<String>,
pub version: String,
pub provider: Provider,
pub resources: BTreeMap<ElementId, Rc<Resource>>,
pub functions: BTreeMap<ElementId, Rc<Function>>,
pub types: BTreeMap<ElementId, Rc<GlobalType>>,
pub resource_name_map: HashMap<String, Rc<Resource>>,
pub function_name_map: HashMap<String, Rc<Function>>,
pub all_types: BTreeMap<ElementId, Rc<GlobalType>>,
}
impl Package {
pub fn new(
name: String,
display_name: Option<String>,
plugin_download_url: Option<String>,
version: String,
provider: Provider,
resources: BTreeMap<ElementId, Resource>,
functions: BTreeMap<ElementId, Function>,
types: BTreeMap<ElementId, GlobalType>,
) -> Self {
let mut resource_name_map = HashMap::new();
let mut new_resources = BTreeMap::new();
for (element_id, resource) in resources {
let mut chunks = Vec::new();
chunks.push(name.clone());
chunks.extend(element_id.namespace.clone());
chunks.push(element_id.name.clone());
let rc = Rc::new(resource);
new_resources.insert(element_id, rc.clone());
let name = chunks.join(":");
resource_name_map.insert(name.clone(), rc.clone());
}
let mut new_function = BTreeMap::new();
let mut function_name_map = HashMap::new();
for (element_id, function) in functions {
let mut chunks = Vec::new();
chunks.push(name.clone());
chunks.extend(element_id.namespace.clone());
chunks.push(element_id.name.clone());
let rc = Rc::new(function);
new_function.insert(element_id, rc.clone());
let name = chunks.join(":");
function_name_map.insert(name.clone(), rc.clone());
}
let mut new_types = BTreeMap::new();
for (element_id, t) in types {
let rc = Rc::new(t);
new_types.insert(element_id.clone(), rc.clone());
}
Self {
name,
display_name,
version,
plugin_download_url,
provider,
resources: new_resources,
functions: new_function,
types: new_types.clone(),
resource_name_map,
function_name_map,
all_types: new_types,
}
}
}
#[derive(Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Debug)]
pub enum Ref {
Type(ElementId),
Archive,
Asset,
Any,
CurrentProvider,
}
#[derive(Clone, serde::Serialize, Debug, PartialEq, Hash, Ord, PartialOrd, Eq)]
pub struct ElementId {
pub namespace: Vec<String>,
pub name: String,
pub raw: String,
}
impl Ref {
pub fn new(raw: &str) -> Result<Self> {
if raw == "pulumi.json#/Archive" {
Ok(Ref::Archive)
} else if raw == "pulumi.json#/Asset" {
Ok(Ref::Asset)
} else if raw == "pulumi.json#/Any" {
Ok(Ref::Any)
} else if raw.starts_with("#/types/") {
Ok(Ref::Type(ElementId::new(
raw.strip_prefix("#/types/")
.context(format!("Cannot strip types prefix from {raw}"))?,
)?))
} else if raw == "#/provider" {
Ok(Ref::CurrentProvider)
} else {
Err(anyhow::anyhow!("Cannot generate ref from [{raw}]."))
}
}
}
impl ElementId {
pub fn new(raw: &str) -> Result<Self> {
let raw = raw.replace("%2F", "/");
let parts: Vec<&str> = raw.split(':').collect();
if parts.len() != 3 {
return Err(anyhow::anyhow!("Cannot generate element id from [{raw}]"));
}
let name = parts[2].to_string();
let namespace = parts[1].split('/').collect::<Vec<_>>();
let mut final_namespaces = Vec::new();
for (i, s) in namespace.iter().enumerate() {
if i == 0 && s == &"index" {
continue;
}
if i == namespace.len() - 1 && s.to_lowercase() == name.to_lowercase() {
continue;
}
if s.is_empty() {
continue;
}
final_namespaces.push(s.to_string());
}
Ok(ElementId {
namespace: final_namespaces,
name,
raw: raw.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use crate::model::ElementId;
#[test]
fn extract_namespace_from_command() {
let id = "command:remote:Connection";
assert_eq!(
ElementId::new(id).unwrap(),
ElementId {
namespace: vec!["remote".to_string()],
name: "Connection".to_string(),
raw: id.to_string(),
}
);
}
#[test]
fn extract_namespace_from_random() {
let id = "random:index/randomBytes:RandomBytes";
assert_eq!(
ElementId::new(id).unwrap(),
ElementId {
namespace: vec![],
name: "RandomBytes".to_string(),
raw: id.to_string(),
}
);
}
#[test]
fn perform_escaping() {
let id = "docker:index%2FContainerPort:ContainerPort";
assert_eq!(
ElementId::new(id).unwrap(),
ElementId {
namespace: vec![],
name: "ContainerPort".to_string(),
raw: "docker:index/ContainerPort:ContainerPort".to_string(),
}
);
}
#[test]
fn should_handle_without_namespace() {
let id = "mypkg::BastionShareableLink";
assert_eq!(
ElementId::new(id).unwrap(),
ElementId {
namespace: vec![],
name: "BastionShareableLink".to_string(),
raw: id.to_string(),
}
);
}
#[test]
fn should_handle_deeply_nested() {
let id = "foo-bar:deeply/nested/module:Resource";
assert_eq!(
ElementId::new(id).unwrap(),
ElementId {
namespace: vec![
"deeply".to_string(),
"nested".to_string(),
"module".to_string()
],
name: "Resource".to_string(),
raw: id.to_string(),
}
)
}
#[test]
fn should_handle_nesting() {
let id = "azure:frontdoor/frontdoor:Frontdoor";
assert_eq!(
ElementId::new(id).unwrap(),
ElementId {
namespace: vec!["frontdoor".to_string()],
name: "Frontdoor".to_string(),
raw: id.to_string(),
}
)
}
}