Skip to content

Commit

Permalink
feat(metagen): union/either for clients (#857)
Browse files Browse the repository at this point in the history
- Add union support for the `client_xx` metagen implementations.

There are still some edge cases especially around variant identification
in the client languages. I tried many things but our hands are tied by
serde. Basically, users will have to be careful when designing their
union types to avoid ambiguity cases. Hopefully,
[674](https://linear.app/metatypedev/issue/MET-674/graph-checksvalidation-on-teither)
will help there.

#### Migration notes

...

- [x] The change comes with new or modified tests
- [x] Hard-to-understand functions have explanatory comments
- [ ] End-user documentation is updated to reflect the change


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced new methods for rendering union types in both TypeScript
and Python.
- Enhanced GraphQL query generation with support for multiple union
types.
- Added a new `variants` property to the `NodeMeta` type for improved
selection handling.
  
- **Bug Fixes**
	- Improved error handling for node selections and argument processing.

- **Tests**
- Updated test cases to reflect schema changes and added new tests for
client functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
Yohe-Am authored Sep 26, 2024
1 parent 24eb721 commit 2b24598
Show file tree
Hide file tree
Showing 19 changed files with 1,575 additions and 439 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"src/meta-cli",
"src/metagen",
"src/metagen/src/fdk_rust/static",
"src/metagen/src/client_rs/static",
"src/mt_deno",
"src/typegate/engine",
"src/typegate/standalone",
Expand Down
24 changes: 20 additions & 4 deletions src/metagen/src/client_py/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ fn render_client_py(_config: &ClienPyGenConfig, tg: &Typegraph) -> anyhow::Resul
};
let name_mapper = Rc::new(name_mapper);

let node_metas = render_node_metas(dest, &manifest, name_mapper.clone())?;
let (node_metas, named_types) = render_node_metas(dest, &manifest, name_mapper.clone())?;
let data_types = render_data_types(dest, &manifest, name_mapper.clone())?;
let data_types = Rc::new(data_types);
let selection_names =
Expand All @@ -147,6 +147,15 @@ class QueryGraph(QueryGraphBase):
"{ty_name}": "{gql_ty}","#
)?;
}
for id in named_types {
let ty_name = &tg.types[id as usize].base().title;
let gql_ty = get_gql_type(&tg.types, id, false);
write!(
dest,
r#"
"{ty_name}": "{gql_ty}","#
)?;
}
write!(
dest,
r#"
Expand Down Expand Up @@ -264,10 +273,14 @@ fn render_node_metas(
dest: &mut GenDestBuf,
manifest: &RenderManifest,
name_mapper: Rc<NameMapper>,
) -> Result<NameMemo> {
) -> Result<(NameMemo, HashSet<u32>)> {
let named_types = Rc::new(std::sync::Mutex::new(HashSet::new()));
let mut renderer = TypeRenderer::new(
name_mapper.nodes.clone(),
Rc::new(node_metas::PyNodeMetasRenderer { name_mapper }),
Rc::new(node_metas::PyNodeMetasRenderer {
name_mapper,
named_types: named_types.clone(),
}),
);
for &id in &manifest.node_metas {
_ = renderer.render(id)?;
Expand All @@ -283,7 +296,10 @@ class NodeDescs:
{methods}
"#
)?;
Ok(memo)
Ok((
memo,
Rc::try_unwrap(named_types).unwrap().into_inner().unwrap(),
))
}

struct NameMapper {
Expand Down
85 changes: 75 additions & 10 deletions src/metagen/src/client_py/node_metas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{interlude::*, shared::types::*};

pub struct PyNodeMetasRenderer {
pub name_mapper: Rc<super::NameMapper>,
pub named_types: Rc<std::sync::Mutex<HashSet<u32>>>,
}

impl PyNodeMetasRenderer {
Expand Down Expand Up @@ -57,8 +58,10 @@ impl PyNodeMetasRenderer {
r#"
@staticmethod
def {ty_name}():
return_node = NodeDescs.{return_node}()
return NodeMeta(
sub_nodes=NodeDescs.{return_node}().sub_nodes,"#
sub_nodes=return_node.sub_nodes,
variants=return_node.variants,"#
)?;
if let Some(fields) = argument_fields {
write!(
Expand All @@ -85,6 +88,37 @@ impl PyNodeMetasRenderer {
dest,
r#"
)
"#
)?;
Ok(())
}

fn render_for_union(
&self,
dest: &mut TypeRenderer,
ty_name: &str,
variants: IndexMap<String, Rc<str>>,
) -> std::fmt::Result {
write!(
dest,
r#"
@staticmethod
def {ty_name}():
return NodeMeta(
variants={{"#
)?;
for (key, node_ref) in variants {
write!(
dest,
r#"
"{key}": NodeDescs.{node_ref},"#
)?;
}
write!(
dest,
r#"
}},
)
"#
)?;
Ok(())
Expand Down Expand Up @@ -114,10 +148,14 @@ impl RenderType for PyNodeMetasRenderer {
| TypeNode::List {
data: ListTypeData { items: item, .. },
..
} => renderer.render_subgraph(*item, cursor)?.0.unwrap().to_string(),
} => renderer
.render_subgraph(*item, cursor)?
.0
.unwrap()
.to_string(),
TypeNode::Function { data, base } => {
let (return_ty_name, _cyclic) = renderer.render_subgraph(data.output, cursor)?;
let return_ty_name = return_ty_name.unwrap() ;
let return_ty_name = return_ty_name.unwrap();
let props = match renderer.nodes[data.input as usize].deref() {
TypeNode::Object { data, .. } if !data.properties.is_empty() => {
let props = data
Expand Down Expand Up @@ -154,16 +192,43 @@ impl RenderType for PyNodeMetasRenderer {
ty_name
}
TypeNode::Either {
..
// data: EitherTypeData { one_of: variants },
// base,
data: EitherTypeData { one_of: variants },
base,
}
| TypeNode::Union {
..
// data: UnionTypeData { any_of: variants },
// base,
data: UnionTypeData { any_of: variants },
base,
} => {
todo!("unions are wip")
let mut named_set = vec![];
let variants = variants
.iter()
.filter_map(|&inner| {
if !renderer.is_composite(inner) {
return None;
}
named_set.push(inner);
let (ty_name, _cyclic) = match renderer.render_subgraph(inner, cursor) {
Ok(val) => val,
Err(err) => return Some(Err(err)),
};
let ty_name = ty_name.unwrap();
Some(eyre::Ok((
renderer.nodes[inner as usize].deref().base().title.clone(),
ty_name,
)))
})
.collect::<Result<IndexMap<_, _>, _>>()?;
if !variants.is_empty() {
{
let mut named_types = self.named_types.lock().unwrap();
named_types.extend(named_set)
}
let ty_name = normalize_type_title(&base.title);
self.render_for_union(renderer, &ty_name, variants)?;
ty_name
} else {
"scalar".into()
}
}
};
Ok(name)
Expand Down
102 changes: 85 additions & 17 deletions src/metagen/src/client_py/selections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,40 @@ impl PyNodeSelectionsRenderer {
writeln!(dest)?;
Ok(())
}

fn render_for_union(
&self,
dest: &mut TypeRenderer,
ty_name: &str,
variants: IndexMap<String, (String, SelectionTy)>,
) -> std::fmt::Result {
writeln!(dest, r#"{ty_name} = typing.TypedDict("{ty_name}", {{"#)?;
writeln!(dest, r#" "_": SelectionFlags,"#)?;
for (_name, (variant_ty, select_ty)) in &variants {
use SelectionTy::*;
match select_ty {
Scalar | ScalarArgs { .. } => {
// scalars always get selected if the union node
// gets selected
unreachable!()
}
Composite { select_ty } => writeln!(
dest,
// use variant_ty as key instead of normalized struct name
// we want it to match the varaint name from the NodeMetas
// later so no normlalization is used
r#" "{variant_ty}": CompositeSelectNoArgs["{select_ty}"],"#
)?,
CompositeArgs { arg_ty, select_ty } => writeln!(
dest,
r#" "{variant_ty}": CompositeSelectArgs["{arg_ty}", "{select_ty}"],"#
)?,
};
}
writeln!(dest, "}}, total=False)")?;
writeln!(dest)?;
Ok(())
}
}

impl RenderType for PyNodeSelectionsRenderer {
Expand All @@ -56,22 +90,32 @@ impl RenderType for PyNodeSelectionsRenderer {
| TypeNode::String { .. }
| TypeNode::File { .. } => unreachable!("scalars don't get to have selections"),
TypeNode::Any { .. } => unimplemented!("Any type support not implemented"),
TypeNode::Optional { data: OptionalTypeData { item, .. }, .. }
| TypeNode::List { data: ListTypeData { items: item, .. }, .. }
| TypeNode::Function { data:FunctionTypeData { output: item, .. }, .. }
=> renderer.render_subgraph(*item, cursor)?.0.unwrap().to_string(),
TypeNode::Optional {
data: OptionalTypeData { item, .. },
..
}
| TypeNode::List {
data: ListTypeData { items: item, .. },
..
}
| TypeNode::Function {
data: FunctionTypeData { output: item, .. },
..
} => renderer
.render_subgraph(*item, cursor)?
.0
.unwrap()
.to_string(),
TypeNode::Object { data, base } => {
let props = data
.properties
.iter()
// generate property types first
.map(|(name, &dep_id)| {
eyre::Ok(
(
normalize_struct_prop_name(name),
selection_for_field(dep_id, &self.arg_ty_names, renderer, cursor)?
)
)
eyre::Ok((
normalize_struct_prop_name(name),
selection_for_field(dep_id, &self.arg_ty_names, renderer, cursor)?,
))
})
.collect::<Result<IndexMap<_, _>, _>>()?;
let node_name = &base.title;
Expand All @@ -81,16 +125,40 @@ impl RenderType for PyNodeSelectionsRenderer {
ty_name
}
TypeNode::Either {
..
// data: EitherTypeData { one_of: variants },
// base,
data: EitherTypeData { one_of: variants },
base,
}
| TypeNode::Union {
..
// data: UnionTypeData { any_of: variants },
// base,
data: UnionTypeData { any_of: variants },
base,
} => {
todo!("unions are wip")
let variants = variants
.iter()
.filter_map(|&inner| {
if !renderer.is_composite(inner) {
return None;
}
let ty_name = renderer.nodes[inner as usize].deref().base().title.clone();
let struct_prop_name =
normalize_struct_prop_name(&normalize_type_title(&ty_name[..]));

let selection = match selection_for_field(
inner,
&self.arg_ty_names,
renderer,
cursor,
) {
Ok(selection) => selection,
Err(err) => return Some(Err(err)),
};

Some(eyre::Ok((struct_prop_name, (ty_name, selection))))
})
.collect::<Result<IndexMap<_, _>, _>>()?;
let ty_name = normalize_type_title(&base.title);
let ty_name = format!("{ty_name}Selections").to_pascal_case();
self.render_for_union(renderer, &ty_name, variants)?;
ty_name
}
};
Ok(name)
Expand Down
Loading

0 comments on commit 2b24598

Please sign in to comment.