implement optional attrs

This commit is contained in:
nora 2024-05-29 21:03:53 +02:00
parent b50fa51e9c
commit bf7835162d
5 changed files with 135 additions and 56 deletions

View file

@ -53,7 +53,7 @@ impl DataSource for ExampleDataSource {
},
"right" => Attribute::String {
description: "meow".to_owned(),
mode: Mode::Required,
mode: Mode::Optional,
sensitive: false,
},
},

View file

@ -42,10 +42,11 @@ pub async fn start<P: Provider>(provider: P) -> eyre::Result<()> {
server::serve(provider).await
}
/// ```rust
/// ```rust,no_run
/// # use std::collections::HashMap;
/// let x: HashMap<String, u8> = terustform::attrs! {
/// "hello" => 0,
/// # let attr: terustform::Attribute = todo!();
/// let x: HashMap<String, terustform::Attribute> = terustform::attrs! {
/// "hello" => attr,
/// };
/// ```
#[macro_export]
@ -53,7 +54,7 @@ macro_rules! attrs {
(
$( $name:literal => $rhs:expr ,)*
) => {
<$crate::__derive_private::HashMap<_, _> as $crate::__derive_private::FromIterator<(_, _)>>::from_iter([
$crate::__derive_private::FromIterator::from_iter([
$(
(
$name.into(),
@ -69,7 +70,7 @@ macro_rules! attrs {
pub mod __derive_private {
pub use crate::{
AttrPath, AttrPathSegment, BaseValue, DResult, Diagnostic, Diagnostics, Value, ValueKind,
ValueModel,
ValueModel, Attribute,
};
pub use {std::collections::HashMap, Clone, FromIterator, Option::Some, Result::Err, ToOwned};

View file

@ -77,13 +77,24 @@ impl Attribute {
}
fn attrs_typ(attrs: &HashMap<String, Attribute>) -> Type {
let attrs = attrs
let attr_tys = attrs
.iter()
.map(|(name, attr)| (name.clone(), attr.typ()))
.collect();
let optionals = attrs
.iter()
.filter_map(|(name, attr)| {
if attr.mode().optional() {
Some(name.clone())
} else {
None
}
})
.collect();
Type::Object {
attrs,
optionals: vec![],
attrs: attr_tys,
optionals,
}
}

View file

@ -1,4 +1,4 @@
use crate::{AttrPathSegment, Attribute, Diagnostics, Mode, Schema, Value};
use crate::{AttrPath, AttrPathSegment, Attribute, Diagnostics, Mode, Schema, Value};
use super::grpc::tfplugin6;
@ -88,9 +88,16 @@ impl Diagnostics {
severity: tfplugin6::diagnostic::Severity::Error as _,
summary: err.msg,
detail: "".to_owned(),
attribute: err.attr.map(|attr| -> tfplugin6::AttributePath {
attribute: err.attr.map(|path| path.into_tfplugin()),
})
.collect()
}
}
impl AttrPath {
pub(crate) fn into_tfplugin(self) -> tfplugin6::AttributePath {
tfplugin6::AttributePath {
steps: attr
steps: self
.0
.into_iter()
.map(|segment| {
@ -98,23 +105,16 @@ impl Diagnostics {
tfplugin6::attribute_path::Step {
selector: Some(match segment {
AttrPathSegment::AttributeName(name) => {
Selector::AttributeName(name)
}
AttrPathSegment::AttributeName(name) => Selector::AttributeName(name),
AttrPathSegment::ElementKeyString(key) => {
Selector::ElementKeyString(key)
}
AttrPathSegment::ElementKeyInt(key) => {
Selector::ElementKeyInt(key)
}
AttrPathSegment::ElementKeyInt(key) => Selector::ElementKeyInt(key),
}),
}
})
.collect(),
}
}),
})
.collect()
}
}

View file

@ -2,7 +2,7 @@
// tftypes for types and values
use std::{
collections::{BTreeMap, HashMap},
collections::BTreeMap,
io::{self, Read},
};
@ -29,7 +29,7 @@ pub enum Type {
/// A bunch of unordered string-value pairs of different types.
/// The attributes are statically known.
Object {
attrs: HashMap<String, Type>,
attrs: BTreeMap<String, Type>,
/// The attributes in `attrs` that are optional.
/// Always empty for now because of JSON reasons.
optionals: Vec<String>,
@ -41,6 +41,8 @@ pub enum Type {
}
impl Type {
// tftypes/type.go
// https://github.com/hashicorp/terraform-plugin-go/blob/05dc75aefa5b71406022d0ac08eca99f44fbf378/tftypes/type.go#L95
pub fn to_json(&self) -> String {
let value = self.to_json_inner();
serde_json::to_string(&value).unwrap()
@ -59,18 +61,25 @@ impl Type {
Self::List { elem } => compound("list", elem.to_json_inner()),
Self::Map { elem } => compound("map", elem.to_json_inner()),
Self::Set { elem } => compound("set", elem.to_json_inner()),
Self::Object {
attrs,
optionals: _,
} => compound(
"object",
Self::Object { attrs, optionals } => {
let mut parts = vec![
Value::String("object".to_owned()),
Value::Object(
attrs
.iter()
.map(|(k, v)| (k.clone(), v.to_json_inner()))
.collect(),
),
),
];
if !optionals.is_empty() {
parts.push(Value::Array(
optionals.iter().map(|v| Value::String(v.clone())).collect(),
))
}
Value::Array(parts)
}
Self::Tuple { elems } => compound(
"tuple",
elems.iter().map(|elem| elem.to_json_inner()).collect(),
@ -356,9 +365,7 @@ impl Value {
ValueKind::Set(elems)
}
Type::Object { attrs, optionals } => {
assert!(optionals.is_empty());
let len = mp::read_map_len(rd)?;
dbg!(len);
if attrs.len() != (len as usize) {
return Err(Diagnostic::error_string(format!(
@ -370,16 +377,21 @@ impl Value {
let elems = (0..len)
.map(|_| -> DResult<_> {
let key = read_string(rd)?;
dbg!(&key);
let typ = attrs.get(&key).ok_or_else(|| {
Diagnostic::error_string(format!("unexpected attribute: '{key}'"))
})?;
dbg!(typ);
let value = Value::msg_unpack_inner(rd, typ)?;
dbg!(&value);
Ok((key, value))
})
.collect::<DResult<BTreeMap<_, _>>>()?;
for expected_attr in attrs.keys() {
let is_ok = elems.contains_key(expected_attr);
if !is_ok && !optionals.contains(expected_attr) {
return Err(Diagnostic::error_string(format!("expected attribute '{expected_attr}', but it was not present")).into())
}
}
ValueKind::Object(elems)
}
Type::Tuple { elems } => {
@ -406,14 +418,69 @@ impl Value {
#[cfg(test)]
mod test {
use std::collections::{BTreeMap, HashMap};
use std::collections::BTreeMap;
use crate::{Type, Value, ValueKind};
#[test]
fn type_json() {
let typs = [
(Type::Bool, "\"bool\""),
(Type::Number, "\"number\""),
(Type::String, "\"string\""),
(Type::Dynamic, "\"dynamic\""),
(
Type::List {
elem: Box::new(Type::String),
},
r#"["list","string"]"#,
),
(
Type::Map {
elem: Box::new(Type::String),
},
r#"["map","string"]"#,
),
(
Type::Set {
elem: Box::new(Type::String),
},
r#"["set","string"]"#,
),
(
Type::Object {
attrs: crate::attrs! {
"meow" => Type::String,
"mrooow" => Type::String,
"uwu" => Type::String,
},
optionals: vec![],
},
r#"["object",{"meow":"string","mrooow":"string","uwu":"string"}]"#,
),
(
Type::Object {
attrs: crate::attrs! {
"meow" => Type::String,
"mrooow" => Type::String,
"uwu" => Type::String,
},
optionals: vec!["uwu".to_owned()],
},
r#"["object",{"meow":"string","mrooow":"string","uwu":"string"},["uwu"]]"#,
),
];
for (typ, expected) in typs {
let actual_str = typ.to_json();
assert_eq!(actual_str, expected);
}
}
#[test]
fn decode_object() {
let typ = Type::Object {
attrs: HashMap::from([
attrs: BTreeMap::from([
("id".into(), Type::String),
("discord_id".into(), Type::String),
("name".into(), Type::String),