use crate::identifier::Ident;
use super::{
MergePriority,
record::{FieldMetadata, FieldPathElem, Include},
typ::Type,
*,
};
type StaticPath = Vec<Ident>;
pub struct Incomplete();
pub struct Complete<'ast>(Option<Ast<'ast>>);
#[derive(Debug)]
pub struct Field<'ast, State> {
state: State,
path: StaticPath,
metadata: FieldMetadata<'ast>,
doc: Option<String>,
contracts: Vec<Type<'ast>>,
}
impl<'ast, A> Field<'ast, A> {
pub fn doc(self, doc: impl AsRef<str>) -> Self {
self.some_doc(Some(doc))
}
pub fn some_doc(mut self, some_doc: Option<impl AsRef<str>>) -> Self {
self.doc = some_doc.map(|s| s.as_ref().to_string());
self
}
pub fn optional(mut self, opt: bool) -> Self {
self.metadata.opt = opt;
self
}
pub fn not_exported(mut self, not_exported: bool) -> Self {
self.metadata.not_exported = not_exported;
self
}
pub fn contract(mut self, contract: impl Into<Type<'ast>>) -> Self {
self.contracts.push(contract.into());
self
}
pub fn contracts<I>(mut self, contracts: I) -> Self
where
I: IntoIterator<Item = Type<'ast>>,
{
self.contracts.extend(contracts);
self
}
pub fn types(mut self, typ: impl Into<Type<'ast>>) -> Self {
self.metadata.annotation.typ = Some(typ.into());
self
}
pub fn priority(mut self, priority: MergePriority) -> Self {
self.metadata.priority = priority;
self
}
pub fn metadata(mut self, metadata: FieldMetadata<'ast>) -> Self {
self.metadata = metadata;
self
}
}
impl<'ast> Field<'ast, Incomplete> {
pub fn path<I, It>(path: It) -> Self
where
I: AsRef<str>,
It: IntoIterator<Item = I>,
{
Field {
state: Incomplete(),
path: path.into_iter().map(|e| e.as_ref().into()).collect(),
metadata: Default::default(),
doc: None,
contracts: Vec::new(),
}
}
pub fn name(name: impl AsRef<str>) -> Self {
Self::path([name])
}
pub fn no_value(self) -> Field<'ast, Complete<'ast>> {
Field {
state: Complete(None),
path: self.path,
metadata: self.metadata,
doc: self.doc,
contracts: self.contracts,
}
}
pub fn value(self, value: impl Into<Ast<'ast>>) -> Field<'ast, Complete<'ast>> {
Field {
state: Complete(Some(value.into())),
path: self.path,
metadata: self.metadata,
doc: self.doc,
contracts: self.contracts,
}
}
}
impl<'ast> Field<'ast, Complete<'ast>> {
pub fn attach(self, alloc: &'ast AstAlloc, record: Record<'ast>) -> Record<'ast> {
let value = self.state;
let field = Field {
state: record,
path: self.path,
metadata: self.metadata,
doc: self.doc,
contracts: self.contracts,
};
match value {
Complete(Some(v)) => field.value(alloc, v),
Complete(None) => field.no_value(alloc),
}
}
}
impl<'ast> Field<'ast, Record<'ast>> {
pub fn no_value(mut self, alloc: &'ast AstAlloc) -> Record<'ast> {
self.finalize_contracts(alloc);
self.metadata.doc = self.doc.map(|s| alloc.alloc_str(&s));
self.state.field_defs.push(record::FieldDef {
path: alloc.alloc_many(
self.path
.into_iter()
.map(|id| FieldPathElem::Ident(id.into())),
),
metadata: self.metadata,
value: None,
pos: TermPos::None,
});
self.state
}
pub fn value(mut self, alloc: &'ast AstAlloc, value: impl Into<Ast<'ast>>) -> Record<'ast> {
self.finalize_contracts(alloc);
self.metadata.doc = self.doc.map(|s| alloc.alloc_str(&s));
self.state.field_defs.push(record::FieldDef {
path: alloc.alloc_many(
self.path
.into_iter()
.map(|id| FieldPathElem::Ident(id.into())),
),
metadata: self.metadata,
value: Some(value.into()),
pos: TermPos::None,
});
self.state
}
fn finalize_contracts(&mut self, alloc: &'ast AstAlloc) {
self.metadata.annotation.contracts = alloc.alloc_many(self.contracts.drain(..));
}
}
#[derive(Debug, Default)]
pub struct Record<'ast> {
field_defs: Vec<record::FieldDef<'ast>>,
includes: Vec<Include<'ast>>,
open: bool,
}
impl<'ast> Record<'ast> {
pub fn new() -> Self {
Record::default()
}
pub fn field(self, name: impl AsRef<str>) -> Field<'ast, Record<'ast>> {
Field {
state: self,
path: vec![Ident::new(name)],
metadata: Default::default(),
doc: None,
contracts: Vec::new(),
}
}
pub fn fields<I, It>(mut self, alloc: &'ast AstAlloc, fields: It) -> Self
where
I: Into<Field<'ast, Complete<'ast>>>,
It: IntoIterator<Item = I>,
{
for f in fields {
self = f.into().attach(alloc, self)
}
self
}
pub fn include(self, ident: LocIdent) -> Self {
self.include_with_metadata(ident, Default::default())
}
pub fn include_with_metadata(mut self, ident: LocIdent, metadata: FieldMetadata<'ast>) -> Self {
self.includes.push(Include { ident, metadata });
self
}
pub fn path<It, I>(self, path: It) -> Field<'ast, Record<'ast>>
where
I: AsRef<str>,
It: IntoIterator<Item = I>,
{
Field {
state: self,
path: path.into_iter().map(|e| Ident::new(e)).collect(),
metadata: Default::default(),
doc: None,
contracts: Vec::new(),
}
}
pub fn open(mut self) -> Self {
self.open = true;
self
}
pub fn set_open(mut self, open: bool) -> Self {
self.open = open;
self
}
pub fn build(self, alloc: &'ast AstAlloc) -> Ast<'ast> {
alloc
.record(record::Record {
field_defs: alloc.alloc_many(self.field_defs),
includes: alloc.alloc_many(self.includes),
open: self.open,
})
.into()
}
pub fn from_iterator<I, It>(alloc: &'ast AstAlloc, fields: It) -> Self
where
I: Into<Field<'ast, Complete<'ast>>>,
It: IntoIterator<Item = I>,
{
Record::new().fields(alloc, fields)
}
}
#[macro_export]
macro_rules! app {
( $alloc:expr, $f:expr , $arg:expr $(,)?) => {
$crate::ast::Ast::from(
$alloc.app(
$crate::ast::Ast::from($f),
std::iter::once($crate::ast::Ast::from($arg))
)
)
};
( $alloc:expr, $f:expr, $arg1:expr $(, $args:expr )+ $(,)?) => {
{
let args = vec![
$crate::ast::Ast::from($arg1)
$(, $crate::ast::Ast::from($args) )+
];
$crate::ast::Ast::from($alloc.app($crate::ast::Ast::from($f), args))
}
};
}
#[macro_export]
macro_rules! primop_app {
( $alloc:expr, $op:expr , $arg:expr $(,)?) => {
$crate::ast::Ast::from(
$alloc.prim_op(
$op,
std::iter::once($crate::ast::Ast::from($arg))
)
)
};
( $alloc:expr, $op:expr, $arg1:expr $(, $args:expr )+ $(,)?) => {
{
let args = vec![
$crate::ast::Ast::from($arg1)
$(, $crate::ast::Ast::from($args) )+
];
$crate::ast::Ast::from($alloc.prim_op($op, args))
}
};
}
#[macro_export]
macro_rules! fun {
($alloc:expr, args=[ $( $args:expr, )+ ], $body:expr) => {
{
let args = vec![
$($crate::ast::pattern::Pattern::any($crate::identifier::LocIdent::from($args)), )+
];
$crate::ast::Ast::from(
$alloc.fun(args, $crate::ast::Ast::from($body))
)
}
};
($alloc:expr, args=[ $( $args:expr, )* ], $next_arg:expr $(, $rest:expr )+) => {
fun!($alloc, args=[ $( $args, )* $next_arg, ] $(, $rest )+)
};
( $alloc:expr, $arg:expr, $body:expr $(,)?) => {
$crate::ast::Ast::from(
$alloc.fun(
std::iter::once($crate::ast::pattern::Pattern::any($crate::identifier::LocIdent::from($arg))),
$crate::ast::Ast::from($body)
)
)
};
( $alloc:expr, $arg1:expr, $arg2:expr $(, $rest:expr )+ $(,)?) => {
fun!($alloc, args=[ $arg1, $arg2, ] $(, $rest )+)
};
}
pub fn var<'ast>(id: impl Into<LocIdent>) -> Ast<'ast> {
Ast::from(Node::Var(id.into()))
}
pub fn enum_tag<'ast>(tag: impl Into<LocIdent>) -> Ast<'ast> {
Ast::from(Node::EnumVariant {
tag: tag.into(),
arg: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::iter;
fn build_simple_record<'ast, I, Id>(alloc: &'ast AstAlloc, fields: I, open: bool) -> Ast<'ast>
where
Id: Into<LocIdent>,
I: IntoIterator<Item = (Id, Node<'ast>)>,
I::IntoIter: ExactSizeIterator,
{
build_record(
alloc,
fields
.into_iter()
.map(|(id, node)| (id, Default::default(), Some(node))),
open,
)
}
fn build_record<'ast, I, Id>(alloc: &'ast AstAlloc, fields: I, open: bool) -> Ast<'ast>
where
Id: Into<LocIdent>,
I: IntoIterator<Item = (Id, FieldMetadata<'ast>, Option<Node<'ast>>)>,
I::IntoIter: ExactSizeIterator,
{
alloc
.record(record::Record {
field_defs: alloc.alloc_many(fields.into_iter().map(|(id, metadata, node)| {
record::FieldDef {
path: FieldPathElem::single_ident_path(alloc, id.into()),
value: node.map(Ast::from),
metadata,
pos: TermPos::None,
}
})),
includes: &[],
open,
})
.into()
}
#[test]
fn trivial() {
let alloc = AstAlloc::new();
let ast: Ast = Record::new()
.field("foo")
.value(&alloc, alloc.string("bar"))
.build(&alloc);
assert_eq!(
ast,
build_simple_record(&alloc, vec![("foo", alloc.string("bar"))], false)
);
}
#[test]
fn from_iter() {
let alloc = AstAlloc::new();
let ast: Ast = Record::from_iterator(
&alloc,
[
Field::name("foo").value(Node::Null),
Field::name("bar").value(Node::Null),
],
)
.build(&alloc);
assert_eq!(
ast,
build_simple_record(
&alloc,
vec![("foo", Node::Null), ("bar", Node::Null),],
false
)
);
}
#[test]
fn some_doc() {
let alloc = AstAlloc::new();
let ast: Ast = Record::from_iterator(
&alloc,
[
Field::name("foo").some_doc(Some("foo")).no_value(),
Field::name("bar").some_doc(None as Option<&str>).no_value(),
Field::name("baz").doc("baz").no_value(),
],
)
.build(&alloc);
assert_eq!(
ast,
build_record(
&alloc,
vec![
(
"foo",
FieldMetadata {
doc: Some(alloc.alloc_str("foo")),
..Default::default()
},
None
),
("bar", Default::default(), None),
(
"baz",
FieldMetadata {
doc: Some(alloc.alloc_str("baz")),
..Default::default()
},
None,
)
],
false,
)
);
}
#[test]
fn fields() {
let alloc = AstAlloc::new();
let ast: Ast = Record::new()
.fields(
&alloc,
[
Field::name("foo").value(alloc.string("foo")),
Field::name("bar").value(alloc.string("bar")),
],
)
.build(&alloc);
assert_eq!(
ast,
build_simple_record(
&alloc,
vec![("foo", alloc.string("foo")), ("bar", alloc.string("bar")),],
false,
)
);
}
#[test]
fn fields_metadata() {
let alloc = AstAlloc::new();
let ast: Ast = Record::new()
.fields(
&alloc,
[
Field::name("foo").optional(true).no_value(),
Field::name("bar").optional(true).no_value(),
],
)
.build(&alloc);
assert_eq!(
ast,
build_record(
&alloc,
vec![
(
"foo",
FieldMetadata {
opt: true,
..Default::default()
},
None,
),
(
"bar",
FieldMetadata {
opt: true,
..Default::default()
},
None,
),
],
false,
)
);
}
#[test]
fn overriding() {
let alloc = AstAlloc::new();
let ast: Ast = Record::new()
.path(vec!["terraform", "required_providers"])
.value(
&alloc,
Record::from_iterator(
&alloc,
[
Field::name("foo").value(Node::Null),
Field::name("bar").value(Node::Null),
],
)
.build(&alloc),
)
.path(vec!["terraform", "required_providers", "foo"])
.value(&alloc, alloc.string("hello world!"))
.build(&alloc);
eprintln!("{ast:?}");
assert_eq!(
ast,
alloc
.record(record::Record {
field_defs: alloc.alloc_many(vec![
record::FieldDef {
path: alloc.alloc_many(vec![
FieldPathElem::Ident("terraform".into()),
FieldPathElem::Ident("required_providers".into())
]),
metadata: Default::default(),
value: Some(
alloc
.record(record::Record {
field_defs: alloc.alloc_many(vec![
record::FieldDef {
path: FieldPathElem::single_ident_path(
&alloc,
"foo".into()
),
metadata: Default::default(),
value: Some(Node::Null.into()),
pos: TermPos::None,
},
record::FieldDef {
path: FieldPathElem::single_ident_path(
&alloc,
"bar".into()
),
metadata: Default::default(),
value: Some(Node::Null.into()),
pos: TermPos::None,
},
]),
..Default::default()
})
.into()
),
pos: TermPos::None,
},
record::FieldDef {
path: alloc.alloc_many(vec![
FieldPathElem::Ident("terraform".into()),
FieldPathElem::Ident("required_providers".into()),
FieldPathElem::Ident("foo".into())
]),
metadata: Default::default(),
value: Some(alloc.string("hello world!").into()),
pos: TermPos::None,
}
]),
..Default::default()
})
.into()
);
}
#[test]
fn open_record() {
let alloc = AstAlloc::new();
let ast: Ast = Record::new().open().build(&alloc);
assert_eq!(ast, alloc.record(record::Record::empty().open()).into());
}
#[test]
fn prio_metadata() {
let alloc = AstAlloc::new();
let ast: Ast = Record::new()
.field("foo")
.priority(MergePriority::Top)
.no_value(&alloc)
.build(&alloc);
assert_eq!(
ast,
build_record(
&alloc,
iter::once((
"foo",
FieldMetadata {
priority: MergePriority::Top,
..Default::default()
},
None,
)),
false
)
);
}
#[test]
fn contract() {
let alloc = AstAlloc::new();
let ast: Ast = Record::new()
.field("foo")
.contract(TypeF::String)
.no_value(&alloc)
.build(&alloc);
assert_eq!(
ast,
build_record(
&alloc,
iter::once((
"foo",
record::FieldMetadata::from(Annotation {
contracts: alloc.alloc_singleton(Type {
typ: TypeF::String,
pos: TermPos::None
}),
..Default::default()
}),
None
)),
false
)
);
}
#[test]
fn exercise_metadata() {
let alloc = AstAlloc::new();
let ast: Ast = Record::new()
.field("foo")
.priority(MergePriority::Bottom)
.doc("foo?")
.contract(TypeF::String)
.types(TypeF::Number)
.optional(true)
.not_exported(true)
.no_value(&alloc)
.build(&alloc);
assert_eq!(
ast,
build_record(
&alloc,
iter::once((
"foo",
FieldMetadata {
doc: Some(alloc.alloc_str("foo?")),
opt: true,
priority: MergePriority::Bottom,
not_exported: true,
annotation: Annotation {
typ: Some(Type {
typ: TypeF::Number,
pos: TermPos::None,
}),
contracts: alloc.alloc_singleton(Type {
typ: TypeF::String,
pos: TermPos::None
}),
},
},
None,
)),
Default::default()
)
);
}
}