mod ident_source;
#[cfg(test)]
mod test;
use std::collections::{HashMap, HashSet, VecDeque};
use bumpalo::Bump;
use petgraph::{algo::greedy_feedback_arc_set, stable_graph::StableDiGraph};
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{parse::Parse, parse::ParseStream, parse_quote, Ident};
use crate::{
entity_model::{
definition_lookup::{EdmItem, NamedEdmItem},
identifiers::{QualifiedName, TargetPath},
ComplexishType, EntityModel, EntitySet, EntityType, EnumType, NavigationProperty,
NavigationPropertyModifier, NavigationPropertyVariant, PrimitiveTypeAlias, Property,
PropertyVariant, Schema, Singleton,
},
entity_model_filter::{EntityModelFilterLookup, EntityTypeInclusion},
EntityModelFilter,
};
use ident_source::{FieldNameSource, ModChildNameSource};
use self::ident_source::to_snake_case;
type ModPathAndContents = (VecDeque<String>, TokenStream);
#[derive(Clone, Copy)]
enum ServiceUrlReference<'a> {
Declare { url: &'a str },
RefParent { level: usize },
}
impl<'a> ServiceUrlReference<'a> {
fn to_stream(&self) -> TokenStream {
match self {
ServiceUrlReference::Declare { url } => quote! {
pub const SERVICE_URL: &'static str = #url;
},
ServiceUrlReference::RefParent { level } => format!(
"use {}::SERVICE_URL;",
std::iter::repeat("super")
.take(*level)
.collect::<Vec<_>>()
.join("::")
)
.parse()
.unwrap(),
}
}
}
pub fn generate_client_module<'ar>(
entity_model: &EntityModel<'ar>,
arena: &'ar Bump,
entity_model_filter: Option<&mut dyn EntityModelFilter>,
) -> TokenStream {
let mut schemas_and_mod_paths: Vec<(VecDeque<String>, &Schema)> = entity_model
.schemas
.iter()
.map(|schema| {
let mod_path = namespace_to_module_path(schema.alias.unwrap_or(schema.namespace));
(mod_path, schema)
})
.collect();
loop {
enum MatchingSegment<'a> {
Start,
Match(&'a str),
NoMatch,
}
use MatchingSegment::*;
let leading_segment =
schemas_and_mod_paths
.iter()
.fold(Start, |prev_seg, (mod_path, _schema)| {
match (prev_seg, mod_path.get(0)) {
(Start, Some(curr_seg)) => Match(curr_seg),
(Match(prev_seg), Some(curr_seg)) => {
if prev_seg == curr_seg {
Match(prev_seg)
} else {
NoMatch
}
}
(_, None) => NoMatch,
(NoMatch, _) => NoMatch,
}
});
if let Match(_) = leading_segment {
for (mod_path, _schema) in &mut schemas_and_mod_paths {
mod_path.pop_front();
}
} else {
break;
}
}
let indirection_points = get_property_indirection_points(entity_model);
let entity_model_filter_lookup =
EntityModelFilterLookup::new(entity_model, entity_model_filter);
let mut modules: Vec<ModPathAndContents> = Vec::new();
let mut item_name_sources: HashMap<Vec<String>, ModChildNameSource> = HashMap::new();
for (mod_path, schema) in schemas_and_mod_paths {
let mut mod_stream = TokenStream::new();
let child_name_source = item_name_sources
.entry(mod_path.iter().map(|s| s.to_string()).collect())
.or_insert_with(|| ModChildNameSource::new(arena));
for &entity_set in &schema.entity_container.entity_sets {
if !entity_model_filter_lookup.includes(entity_set) {
continue;
}
let syn_entity_set_struct = gen_entity_set(entity_set, child_name_source);
syn_entity_set_struct.to_tokens(&mut mod_stream);
}
for &singleton in &schema.entity_container.singletons {
if !entity_model_filter_lookup.includes(singleton) {
continue;
}
let syn_singleton_struct = gen_singleton(singleton, child_name_source);
syn_singleton_struct.to_tokens(&mut mod_stream);
}
for &complex_type in &schema.complex_types {
if !entity_model_filter_lookup.includes(complex_type) {
continue;
}
let syn_struct = gen_complexish_type_struct(
&complex_type.0,
&indirection_points,
child_name_source,
&entity_model_filter_lookup,
);
syn_struct.to_tokens(&mut mod_stream);
}
for &entity_type in &schema.entity_types {
match entity_model_filter_lookup.includes(entity_type) {
EntityTypeInclusion::IncludeFull => {
let syn_struct = gen_complexish_type_struct(
&entity_type.complex_type_fields,
&indirection_points,
child_name_source,
&entity_model_filter_lookup,
);
syn_struct.to_tokens(&mut mod_stream);
let entity_properties_impl =
gen_entity_properties_impl(&entity_type, child_name_source);
entity_properties_impl.to_tokens(&mut mod_stream);
}
EntityTypeInclusion::IncludeStub => {
let syn_struct = gen_complexish_type_stub(
&entity_type.complex_type_fields,
child_name_source,
);
syn_struct.to_tokens(&mut mod_stream);
}
EntityTypeInclusion::Exclude => {
continue;
}
}
}
for &primitive_alias in &schema.primitive_aliases {
if !entity_model_filter_lookup.includes(primitive_alias) {
continue;
}
let syn_newtype = gen_primitive_alias_newtype(primitive_alias, child_name_source);
syn_newtype.to_tokens(&mut mod_stream);
}
for &enum_type in &schema.enum_types {
if !entity_model_filter_lookup.includes(enum_type) {
continue;
}
let syn_enum = gen_enum_type(enum_type, child_name_source);
syn_enum.to_tokens(&mut mod_stream);
}
modules.push((mod_path, mod_stream));
}
let service_url = ServiceUrlReference::Declare {
url: &entity_model.service_url,
};
build_submodules(modules, service_url)
}
fn namespace_to_module_path(namespace: &str) -> VecDeque<String> {
namespace.split('.').map(to_snake_case).collect()
}
fn get_property_indirection_points<'ar>(
entity_model: &EntityModel<'ar>,
) -> HashSet<TargetPath<'ar>> {
let mut graph = StableDiGraph::new();
let mut node_indices = HashMap::new();
for schema in &entity_model.schemas {
for entity_type in &schema.entity_types {
let node_index = graph.add_node(entity_type.name());
node_indices.insert(entity_type.name(), node_index);
}
for complex_type in &schema.complex_types {
let node_index = graph.add_node(complex_type.name());
node_indices.insert(complex_type.name(), node_index);
}
}
for schema in &entity_model.schemas {
for entity_type in &schema.entity_types {
let src_node_index = node_indices[&entity_type.name()];
for prop in &entity_type.complex_type_fields.properties {
if let PropertyVariant::Complex(complex_type) = prop.r#type.get() {
let tgt_node_index = node_indices[&complex_type.name()];
graph.add_edge(src_node_index, tgt_node_index, prop.name());
}
}
}
for complex_type in &schema.complex_types {
let src_node_index = node_indices[&complex_type.name()];
for prop in &complex_type.0.properties {
if let PropertyVariant::Complex(complex_type) = prop.r#type.get() {
let tgt_node_index = node_indices[&complex_type.name()];
graph.add_edge(src_node_index, tgt_node_index, prop.name());
}
}
}
}
let indirected_props = greedy_feedback_arc_set(&graph);
indirected_props.map(|e| *e.weight()).collect()
}
fn to_ident(s: &str) -> Ident {
syn::parse_str(s).unwrap()
}
fn build_submodules(
submodules: Vec<ModPathAndContents>,
service_url: ServiceUrlReference,
) -> TokenStream {
let mut submod_groups: Vec<(String, Vec<ModPathAndContents>)> = Vec::new();
let mut direct_members: Vec<TokenStream> = Vec::new();
for (mut namespace, mod_contents) in submodules {
if namespace.is_empty() {
direct_members.push(mod_contents);
continue;
}
let existing_group = submod_groups
.iter_mut()
.find(|(first_seg, _members)| first_seg == &namespace[0]);
match existing_group {
Some((_first_seg, members)) => {
namespace.pop_front().unwrap();
members.push((namespace, mod_contents));
}
None => {
let subgroup_mod_name = namespace.pop_front().unwrap();
submod_groups.push((subgroup_mod_name, vec![(namespace, mod_contents)]));
}
};
}
let add_import_statements = !direct_members.is_empty();
let add_service_url_ref =
!direct_members.is_empty() || matches!(service_url, ServiceUrlReference::Declare { .. });
let service_url_ref = if add_service_url_ref {
service_url.to_stream()
} else {
quote! {}
};
let imports = if add_import_statements {
module_imports()
} else {
quote! {}
};
let child_service_url = match service_url {
ServiceUrlReference::Declare { .. } => ServiceUrlReference::RefParent { level: 1 },
ServiceUrlReference::RefParent { level } => {
ServiceUrlReference::RefParent { level: level + 1 }
}
};
let submodules: TokenStream = submod_groups
.into_iter()
.map(|(mod_name, contents)| {
let mod_ident = to_ident(&mod_name);
let mod_contents = build_submodules(contents, child_service_url);
quote! {
pub mod #mod_ident {
#mod_contents
}
}
})
.collect();
let direct_members: TokenStream = direct_members.into_iter().collect();
quote! {
#imports
#service_url_ref
#submodules
#direct_members
}
}
struct ParseableNamedField {
pub field: syn::Field,
}
impl Parse for ParseableNamedField {
fn parse(input: ParseStream<'_>) -> Result<Self, syn::Error> {
let field = syn::Field::parse_named(input)?;
Ok(ParseableNamedField { field })
}
}
fn gen_complexish_type_struct<'ar, BaseType: EdmItem + NamedEdmItem<Name = QualifiedName<'ar>>>(
complexish_type: &ComplexishType<'ar, BaseType>,
indirection_points: &HashSet<TargetPath<'ar>>,
child_name_source: &mut ModChildNameSource<'ar>,
entity_model_filter_lookup: &EntityModelFilterLookup<'ar>,
) -> syn::ItemStruct {
let ComplexishType {
name,
base_type,
is_open_type,
is_abstract: _,
properties,
nav_properties,
} = &complexish_type;
let struct_name = child_name_source.get_type_name(name.uq_name);
let struct_ident = to_ident(struct_name);
let item_attrs = type_item_attributes();
let mut item_struct: syn::ItemStruct = parse_quote! {
#item_attrs
pub struct #struct_ident {}
};
let fields = match item_struct.fields {
syn::Fields::Named(ref mut f) => f,
_ => panic!("Unexpected struct fields type"),
};
let mut field_name_source = FieldNameSource::new();
if let Some(base_type) = base_type {
let base_type_field =
gen_base_type_field(base_type.get(), &mut field_name_source, child_name_source);
fields.named.push(base_type_field);
}
for prop in properties {
let is_indirected = indirection_points.contains(&prop.name());
let field = gen_property_field(
prop,
is_indirected,
&mut field_name_source,
child_name_source,
);
fields.named.push(field);
}
for nav_prop in nav_properties {
let field = gen_nav_property_field(
nav_prop,
&mut field_name_source,
child_name_source,
entity_model_filter_lookup,
);
fields.named.push(field);
}
if *is_open_type {
}
item_struct
}
fn gen_base_type_field<'ar, BaseType: NamedEdmItem<Name = QualifiedName<'ar>>>(
base_type: &BaseType,
field_name_source: &mut FieldNameSource,
mod_child_name_source: &mut ModChildNameSource<'ar>,
) -> syn::Field {
let base_field_name = field_name_source.get_struct_field_name("base");
let base_field_ident = to_ident(base_field_name);
let base_type_name = mod_child_name_source.get_type_name(base_type.name().uq_name);
let field_type: syn::Type = syn::Type::Verbatim(
base_type_name
.parse()
.expect("Failed to parse type str as token stream"),
);
let base_type_field: ParseableNamedField = parse_quote! {
#[serde(flatten)]
pub #base_field_ident: #field_type
};
base_type_field.field
}
fn gen_property_field<'ar>(
prop: &Property<'ar>,
is_indirected: bool,
field_name_source: &mut FieldNameSource,
mod_child_name_source: &mut ModChildNameSource<'ar>,
) -> syn::Field {
let prop_name = prop.name().path;
let field_name = field_name_source.get_struct_field_name(prop_name);
let field_ident = to_ident(field_name);
let inner_type_name = match prop.r#type.get() {
PropertyVariant::Primitive(p) => p.value_representation_type_name(),
PropertyVariant::Abstract => panic!("Abstract EDM type handling not implemented"),
PropertyVariant::PrimitiveAlias(a) => mod_child_name_source.get_type_name(a.name().uq_name),
PropertyVariant::Complex(c) => mod_child_name_source.get_type_name(c.name().uq_name),
PropertyVariant::Enumeration { enum_type, .. } => {
mod_child_name_source.get_type_name(enum_type.name.uq_name)
}
};
let inner_type = syn::Type::Verbatim(
inner_type_name
.parse()
.expect("Failed to parse type str as token stream"),
);
let field_type = match (prop.nullable, prop.is_collection, is_indirected) {
(false, false, false) => inner_type,
(false, false, true) => parse_quote!(Box<#inner_type>),
(false, true, _) => parse_quote!(Vec<#inner_type>),
(true, false, false) => parse_quote!(Option<#inner_type>),
(true, false, true) => parse_quote!(Option<Box<#inner_type>>),
(true, true, _) => parse_quote!(Vec<Option<#inner_type>>),
};
let serde_as_attr = match prop.r#type.get() {
PropertyVariant::Primitive(prim) => {
if let Some(inner_type_name) = prim.deserialize_as_path() {
let serde_as_path = match (prop.nullable, prop.is_collection, is_indirected) {
(false, false, false) => inner_type_name.to_string(),
(false, false, true) => format!("Box<{}>", inner_type_name),
(false, true, _) => format!("Vec<{}>", inner_type_name),
(true, false, false) => format!("Option<{}>", inner_type_name),
(true, false, true) => format!("Option<Box<{}>>", inner_type_name),
(true, true, _) => format!("Vec<Option<{}>>", inner_type_name),
};
quote! {
#[serde_as(as = #serde_as_path)]
}
} else {
quote! {}
}
}
_ => quote! {},
};
let serde_rename_attr = if field_name != prop_name {
quote! {
#[serde(rename = #prop_name)]
}
} else {
quote! {}
};
let prop_field: ParseableNamedField = parse_quote! {
#serde_rename_attr
#serde_as_attr
pub #field_ident: #field_type
};
prop_field.field
}
fn gen_nav_property_field<'ar>(
nav_prop: &NavigationProperty<'ar>,
field_name_source: &mut FieldNameSource,
mod_child_name_source: &mut ModChildNameSource<'ar>,
entity_model_filter_lookup: &EntityModelFilterLookup<'ar>,
) -> syn::Field {
let nav_prop_path = &nav_prop.name().path;
let field_name = field_name_source.get_struct_field_name(nav_prop_path);
let field_ident = to_ident(field_name);
let field_type =
nav_property_field_type(nav_prop, mod_child_name_source, entity_model_filter_lookup);
let serde_rename_attr = if &field_name != nav_prop_path {
quote! {
#[serde(rename = #nav_prop_path)]
}
} else {
quote! {}
};
let prop_field: ParseableNamedField = parse_quote! {
#serde_rename_attr
pub #field_ident: #field_type
};
prop_field.field
}
fn nav_property_field_type<'ar>(
nav_prop: &NavigationProperty<'ar>,
mod_child_name_source: &mut ModChildNameSource<'ar>,
entity_model_filter_lookup: &EntityModelFilterLookup<'ar>,
) -> syn::Type {
let entity_type = match &nav_prop.r#type {
NavigationPropertyVariant::Abstract => panic!("Abstract EDM type handling not implemented"),
NavigationPropertyVariant::Entity(e) => e.get(),
};
let inner_type = syn::Type::Verbatim(
mod_child_name_source
.get_type_name(entity_type.name().uq_name)
.parse()
.expect("Failed to parse type str as token stream"),
);
let link_type = match entity_model_filter_lookup.includes(entity_type) {
EntityTypeInclusion::IncludeFull => syn::Type::Verbatim("EntityLink".parse().unwrap()),
EntityTypeInclusion::IncludeStub => syn::Type::Verbatim("EntityLinkStub".parse().unwrap()),
EntityTypeInclusion::Exclude => {
panic!(
"{} field type {} not included in codegen (should be included fully or as stub)",
nav_prop.name(),
entity_type.name()
)
}
};
match (nav_prop.contains_target, &nav_prop.type_modifier) {
(false, NavigationPropertyModifier::NonNull) => parse_quote!(#link_type<#inner_type>),
(false, NavigationPropertyModifier::Nullable) => {
parse_quote!(Option<#link_type<#inner_type>>)
}
(false, NavigationPropertyModifier::Collection) => {
parse_quote!(Vec<#link_type<#inner_type>>)
}
(true, NavigationPropertyModifier::NonNull) => inner_type,
(true, NavigationPropertyModifier::Nullable) => parse_quote!(Option<#inner_type>),
(true, NavigationPropertyModifier::Collection) => parse_quote!(Vec<#inner_type>),
}
}
fn gen_entity_properties_impl<'ar>(
entity_type: &EntityType<'ar>,
child_name_source: &mut ModChildNameSource<'ar>,
) -> syn::ItemImpl {
let type_name = child_name_source.get_type_name(entity_type.name().uq_name);
let type_ident = to_ident(type_name);
let expand_query: syn::Expr = match gen_expand_query_string(entity_type) {
s if s.is_empty() => parse_quote!(ExpandQuery::None),
s => parse_quote!(ExpandQuery::Expand(#s)),
};
parse_quote! {
impl EntityProperties for #type_ident {
const EXPAND_QUERY: ExpandQuery = #expand_query;
}
}
}
fn gen_expand_query_string(entity_type: &EntityType) -> String {
let mut segments = Vec::new();
for nav_prop in &entity_type.complex_type_fields.nav_properties {
if nav_prop.contains_target {
let nav_prop_type = match &nav_prop.r#type {
NavigationPropertyVariant::Abstract => panic!(), NavigationPropertyVariant::Entity(e) => e.get(),
};
let sub_expand_query = gen_expand_query_string(nav_prop_type);
if sub_expand_query.is_empty() {
segments.push(nav_prop.name.path.to_string());
} else {
segments.push(format!(
"{}($expand={})",
&nav_prop.name.path, sub_expand_query
));
}
} else {
segments.push(format!("{}/$ref", &nav_prop.name.path));
}
}
segments.join(",")
}
fn gen_complexish_type_stub<'ar, BaseType: EdmItem + NamedEdmItem<Name = QualifiedName<'ar>>>(
complexish_type: &ComplexishType<'ar, BaseType>,
child_name_source: &mut ModChildNameSource<'ar>,
) -> syn::ItemStruct {
let struct_name = child_name_source.get_type_name(complexish_type.name.uq_name);
let struct_ident = to_ident(struct_name);
parse_quote! {
#[derive(Debug)]
pub struct #struct_ident;
}
}
fn gen_primitive_alias_newtype<'ar>(
primitive_alias: &PrimitiveTypeAlias<'ar>,
child_name_source: &mut ModChildNameSource<'ar>,
) -> syn::ItemStruct {
let struct_name = child_name_source.get_type_name(primitive_alias.name().uq_name);
let struct_ident = to_ident(struct_name);
let inner_type_name = primitive_alias.r#type.value_representation_type_name();
let inner_type_ident = to_ident(inner_type_name);
let item_attrs = type_item_attributes();
parse_quote! {
#item_attrs
#[serde(transparent)]
pub struct #struct_ident(pub #inner_type_ident);
}
}
fn gen_enum_type<'ar>(
enum_type: &EnumType<'ar>,
child_name_source: &mut ModChildNameSource<'ar>,
) -> syn::ItemEnum {
let enum_type_name = child_name_source.get_type_name(enum_type.name.uq_name);
let enum_type_ident = to_ident(enum_type_name);
let item_attrs = type_item_attributes();
let mut item_enum: syn::ItemEnum = parse_quote! {
#item_attrs
pub enum #enum_type_ident {}
};
let mut field_name_source = FieldNameSource::new();
for member in &enum_type.members {
let edm_member_name = &member.name;
let variant_name = field_name_source.get_enum_variant_name(edm_member_name);
let variant_ident = to_ident(&variant_name);
let variant_value = syn::LitInt::new(&format!("{}", member.value), Span::call_site());
let serde_rename_attr = if &variant_name != edm_member_name {
quote! {
#[serde(rename = #edm_member_name)]
}
} else {
quote! {}
};
let variant: syn::Variant = parse_quote! {
#serde_rename_attr
#variant_ident = #variant_value
};
item_enum.variants.push(variant);
}
item_enum
}
fn gen_entity_set<'ar>(
entity_set: &EntitySet<'ar>,
child_name_source: &mut ModChildNameSource<'ar>,
) -> syn::ItemConst {
let entity_set_path = entity_set.name().path;
let const_item_name = child_name_source.get_const_item_name(entity_set_path);
let const_item_ident = to_ident(const_item_name);
let type_name = child_name_source.get_type_name(entity_set.entity_type.get().name().uq_name);
let type_ident = to_ident(type_name);
parse_quote! {
#[allow(dead_code)]
pub const #const_item_ident: EntitySetEndpoint<#type_ident> = EntitySetEndpoint {
service_url: SERVICE_URL,
name: #entity_set_path,
marker: PhantomData
};
}
}
fn gen_singleton<'ar>(
singleton: &Singleton<'ar>,
child_name_source: &mut ModChildNameSource<'ar>,
) -> syn::ItemConst {
let singleton_path = singleton.name().path;
let const_item_name = child_name_source.get_const_item_name(singleton_path);
let const_item_ident = to_ident(const_item_name);
let type_name = child_name_source.get_type_name(singleton.entity_type.get().name().uq_name);
let type_ident = to_ident(type_name);
parse_quote! {
#[allow(dead_code)]
pub const #const_item_ident: SingletonEndpoint<#type_ident> = SingletonEndpoint {
service_url: SERVICE_URL,
name: #singleton_path,
marker: PhantomData
};
}
}
fn type_item_attributes() -> TokenStream {
parse_quote! {
#[serde_as(crate = "odata_client::serde_with")]
#[derive(Debug, Deserialize)]
#[serde(crate = "odata_client::serde")]
}
}
fn module_imports() -> TokenStream {
quote! {
#[allow(unused_imports)]
use odata_client::{
EntityProperties,
EntitySetEndpoint,
ExpandQuery,
SingletonEndpoint,
EntityLink,
EntityLinkStub,
deserialize_with,
serde_with::serde_as,
serde::Deserialize,
chrono::{
FixedOffset,
DateTime,
NaiveDate,
NaiveTime
},
iso8601,
uuid,
};
use std::marker::PhantomData;
}
}