compiler-rs/clients_schema/src/transform/expand_generics.rs (375 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. use std::collections::{HashMap, HashSet}; use anyhow::bail; use indexmap::IndexMap; use crate::*; #[derive(Default)] struct Ctx { new_types: IndexMap<TypeName, TypeDefinition>, types_seen: std::collections::HashSet<TypeName>, config: ExpandConfig, } /// Generic parameters of a type type GenericParams = Vec<TypeName>; /// Generic arguments for an instantiated generic type type GenericArgs = Vec<ValueOf>; /// Mapping from generic arguments to values type GenericMapping = HashMap<TypeName, ValueOf>; #[derive(Clone, Debug)] pub struct ExpandConfig { /// Generic types that will be inlined by replacing them with their definition, propagating generic arguments. pub unwrap: HashSet<TypeName>, // Generic types that will be unwrapped by replacing them with their (single) generic parameter. pub inline: HashSet<TypeName>, } impl Default for ExpandConfig { fn default() -> Self { ExpandConfig { unwrap: Default::default(), inline: HashSet::from([builtins::WITH_NULL_VALUE]) } } } /// Expand all generics by creating new concrete types for every instantiation of a generic type. /// /// The resulting model has no generics anymore. Top-level generic parameters (e.g. SearchRequest's TDocument) are /// replaced by user_defined_data. pub fn expand(model: IndexedModel, config: ExpandConfig) -> anyhow::Result<IndexedModel> { let mut model = model; let mut ctx = Ctx { config, ..Ctx::default() }; for endpoint in &model.endpoints { for name in [&endpoint.request, &endpoint.response].into_iter().flatten() { expand_root_type(name, &model, &mut ctx)?; } } // Add new types that were created to the model ctx.new_types.sort_keys(); model.types = ctx.new_types; return Ok(model); //--------------------------------------------------------------------------------------------- // Expanding types //--------------------------------------------------------------------------------------------- fn expand_root_type(t: &TypeName, model: &IndexedModel, ctx: &mut Ctx) -> anyhow::Result<()> { const NO_GENERICS: &Vec<TypeName> = &Vec::new(); const USER_DEFINED: ValueOf = ValueOf::UserDefinedValue(UserDefinedValue {}); use TypeDefinition::*; let generics = match model.get_type(t)? { Interface(itf) => &itf.generics, Request(req) => &req.generics, Response(resp) => &resp.generics, Enum(_) | TypeAlias(_) => NO_GENERICS, }; // Top-level generic parameters (e.g. TDocument in SearchResponse) are set to UserDefined let args: GenericArgs = generics.iter().map(|_| USER_DEFINED).collect(); expand_type(t, args, model, ctx)?; Ok(()) } /// Expand a type definition, given concrete values for its generic parameters. /// The new type definition is stored in the context. /// /// Returns the name to use for this (type, args) combination fn expand_type( name: &TypeName, args: GenericArgs, model: &IndexedModel, ctx: &mut Ctx, ) -> anyhow::Result<TypeName> { if name.is_builtin() { return Ok(name.clone()); } let def = model.get_type(name)?; let name = expanded_name(def.name(), &args); if !ctx.types_seen.contains(&name) { // Mark it as seen to avoid infinite recursion ctx.types_seen.insert(name.clone()); let mut new_def = match def { TypeDefinition::Interface(ref itf) => expand_interface(itf, args, model, ctx)?, TypeDefinition::Request(req) => expand_request(req, args, model, ctx)?, TypeDefinition::Response(resp) => expand_response(resp, args, model, ctx)?, TypeDefinition::TypeAlias(ref alias) => expand_type_alias(alias, args, model, ctx)?, TypeDefinition::Enum(_) => def.clone(), }; new_def.base_mut().name = name.clone(); ctx.new_types.insert(name.clone(), new_def); } Ok(name) } fn expand_interface( itf: &Interface, args: GenericArgs, model: &IndexedModel, ctx: &mut Ctx, ) -> anyhow::Result<TypeDefinition> { // Clone and modify in place let mut itf = itf.clone(); let mappings = param_mapping(&itf.generics, args); itf.generics = Vec::new(); if let Some(inherit) = itf.inherits { itf.inherits = Some(expand_inherits(inherit, &mappings, model, ctx)?); } if !itf.behaviors.is_empty() { itf.behaviors.iter().for_each(|b| { if b.generics.is_empty() { // If the behavior has no generics, we can just expand it let _ = expand_type(&b.typ, Vec::new(), model, ctx); } }); } // We keep the generic parameters of behaviors, but expand their value for behavior in &mut itf.behaviors { for arg in &mut behavior.generics { *arg = expand_valueof(arg, &mappings, model, ctx)?; } } expand_properties(&mut itf.properties, &mappings, model, ctx)?; Ok(itf.into()) } fn expand_request( req: &Request, args: GenericArgs, model: &IndexedModel, ctx: &mut Ctx, ) -> anyhow::Result<TypeDefinition> { // Clone and modify in place let mut req = req.clone(); let mappings = param_mapping(&req.generics, args); req.generics = Vec::new(); if let Some(inherit) = req.inherits { req.inherits = Some(expand_inherits(inherit, &mappings, model, ctx)?); } expand_behaviors(&mut req.behaviors, &mappings, model, ctx)?; expand_properties(&mut req.path, &mappings, model, ctx)?; expand_properties(&mut req.query, &mappings, model, ctx)?; expand_body(&mut req.body, &mappings, model, ctx)?; Ok(req.into()) } fn expand_response( resp: &Response, args: GenericArgs, model: &IndexedModel, ctx: &mut Ctx, ) -> anyhow::Result<TypeDefinition> { // Clone and modify in place let mut resp = resp.clone(); let mappings = param_mapping(&resp.generics, args); resp.generics = Vec::new(); expand_behaviors(&mut resp.behaviors, &mappings, model, ctx)?; expand_body(&mut resp.body, &mappings, model, ctx)?; // TODO: exceptions Ok(resp.into()) } fn expand_type_alias( t: &TypeAlias, args: GenericArgs, model: &IndexedModel, ctx: &mut Ctx, ) -> anyhow::Result<TypeDefinition> { let mapping = param_mapping(&t.generics, args); let value = expand_valueof(&t.typ, &mapping, model, ctx)?; Ok(TypeDefinition::TypeAlias(TypeAlias { base: t.base.clone(), generics: Vec::new(), typ: value, variants: t.variants.clone(), })) } //--------------------------------------------------------------------------------------------- // Expanding type parts in place //--------------------------------------------------------------------------------------------- fn expand_inherits( i: Inherits, mappings: &GenericMapping, model: &IndexedModel, ctx: &mut Ctx, ) -> anyhow::Result<Inherits> { let expanded = expand_valueof( &InstanceOf { typ: i.typ, generics: i.generics, } .into(), mappings, model, ctx, )?; if let ValueOf::InstanceOf(inst) = expanded { Ok(Inherits { typ: inst.typ, generics: Vec::new(), }) } else { bail!("Inherits clause doesn't expand to an instance_of: {:?}", &expanded); } } /// Expand behaviors in place fn expand_behaviors( behaviors: &mut Vec<Inherits>, mappings: &GenericMapping, model: &IndexedModel, ctx: &mut Ctx, ) -> anyhow::Result<()> { // We keep the generic parameters of behaviors, but expand their value for behavior in behaviors { for arg in &mut behavior.generics { *arg = expand_valueof(arg, mappings, model, ctx)?; } } Ok(()) } /// Expand properties in place fn expand_properties( props: &mut Vec<Property>, mappings: &GenericMapping, model: &IndexedModel, ctx: &mut Ctx, ) -> anyhow::Result<()> { for prop in props { prop.typ = expand_valueof(&prop.typ, mappings, model, ctx)?; } Ok(()) } /// Expand body in place fn expand_body( body: &mut Body, mappings: &GenericMapping, model: &IndexedModel, ctx: &mut Ctx, ) -> anyhow::Result<()> { match body { Body::Value(ref mut value) => { value.value = expand_valueof(&value.value, mappings, model, ctx)?; } Body::Properties(ref mut prop_body) => { expand_properties(&mut prop_body.properties, mappings, model, ctx)?; } Body::NoBody(_) => {} } Ok(()) } //--------------------------------------------------------------------------------------------- // Expanding values //--------------------------------------------------------------------------------------------- fn expand_valueof( value: &ValueOf, mappings: &GenericMapping, model: &IndexedModel, ctx: &mut Ctx, ) -> anyhow::Result<ValueOf> { match value { ValueOf::ArrayOf(ref arr) => { let value = expand_valueof(&arr.value, mappings, model, ctx)?; Ok(ArrayOf { value: Box::new(value) }.into()) } ValueOf::DictionaryOf(dict) => { let key = expand_valueof(&dict.key, mappings, model, ctx)?; let value = expand_valueof(&dict.value, mappings, model, ctx)?; Ok(DictionaryOf { single_key: dict.single_key, key: Box::new(key), value: Box::new(value), } .into()) } ValueOf::InstanceOf(inst) => { // If this is a generic parameter, return its mapping if let Some(p) = mappings.get(&inst.typ) { return Ok(p.clone()); } // Inline or unwrap if required by the config if ctx.config.inline.contains(&inst.typ) { return inline_generic_type(inst, mappings, model, ctx); } if ctx.config.unwrap.contains(&inst.typ) { return unwrap_generic_type(inst, mappings, model, ctx); } // Expand generic parameters, if any let args = inst .generics .iter() .map(|arg| expand_valueof(arg, mappings, model, ctx)) .collect::<Result<Vec<_>, _>>()?; Ok(InstanceOf { typ: expand_type(&inst.typ, args, model, ctx)?, generics: Vec::new(), } .into()) } ValueOf::UnionOf(u) => { let items = u .items .iter() .map(|item| expand_valueof(item, mappings, model, ctx)) .collect::<Result<Vec<_>, _>>()?; Ok(UnionOf { items }.into()) } ValueOf::UserDefinedValue(_) => Ok(value.clone()), ValueOf::LiteralValue(_) => Ok(value.clone()), } } /// Inlines a value of a generic type by replacing it with its definition, propagating /// generic arguments. fn inline_generic_type( value: &InstanceOf, _mappings: &GenericMapping, model: &IndexedModel, ctx: &mut Ctx, ) -> anyhow::Result<ValueOf> { // It has to be an alias (e.g. WithNullValue) if let TypeDefinition::TypeAlias(inline_def) = model.get_type(&value.typ)? { // Create mappings to resolve types in the inlined type's definition let mut inline_mappings = GenericMapping::new(); for (source, dest) in inline_def.generics.iter().zip(value.generics.iter()) { inline_mappings.insert(source.clone(), dest.clone()); } // and expand the inlined type's alias definition let result = expand_valueof(&inline_def.typ, &inline_mappings, model, ctx)?; return Ok(result); } else { bail!("Expecting inlined type {} to be an alias", &value.typ); } } /// Unwraps a value of a generic type by replacing it with its generic parameter fn unwrap_generic_type( value: &InstanceOf, mappings: &GenericMapping, model: &IndexedModel, ctx: &mut Ctx, ) -> anyhow::Result<ValueOf> { // It has to be an alias (e.g. Stringified) if let TypeDefinition::TypeAlias(_unwrap_def) = model.get_type(&value.typ)? { // Expand the inlined type's generic argument (there must be exactly one) if value.generics.len() != 1 { bail!("Expecting unwrapped type {} to have exactly one generic parameter", &value.typ); } let result = expand_valueof(&value.generics[0], mappings, model, ctx)?; return Ok(result); } else { bail!("Expecting unwrapped type {} to be an alias", &value.typ); } } //--------------------------------------------------------------------------------------------- // Misc //--------------------------------------------------------------------------------------------- /// Builds the mapping from generic parameter name to actual value fn param_mapping(generics: &GenericParams, args: GenericArgs) -> GenericMapping { generics.iter().cloned().zip(args).collect() } /// Creates an expanded type name if needed (i.e. when `generics` is not empty) fn expanded_name(type_name: &TypeName, args: &GenericArgs) -> TypeName { if args.is_empty() { return type_name.clone(); } let mut name: String = type_name.name.to_string(); for arg in args { if let ValueOf::UserDefinedValue(_) = arg { // Top-level types. Don't append it. } else { push_valueof_name(&mut name, arg); } } TypeName { namespace: type_name.namespace.clone(), name: name.into(), } } /// Appends the type representation of a value to a string fn push_valueof_name(name: &mut String, value: &ValueOf) { use std::fmt::Write; match value { ValueOf::LiteralValue(lit) => write!(name, "{}", lit).unwrap(), ValueOf::UserDefinedValue(_) => write!(name, "UserDefined").unwrap(), ValueOf::ArrayOf(a) => { name.push_str("Array"); push_valueof_name(name, a.value.as_ref()); } ValueOf::DictionaryOf(dict) => { // Don't care about key, it's always aliased to string name.push_str("Dict"); push_valueof_name(name, dict.value.as_ref()) } ValueOf::UnionOf(u) => { name.push_str("Union"); for item in &u.items { push_valueof_name(name, item) } } ValueOf::InstanceOf(inst) => { // Append unqualified name (assuming we have no duplicate generic value for the same type) name.push_str(&inst.typ.name); } } } } #[cfg(test)] mod tests { use std::io::Write; use super::*; #[test] pub fn compare_with_js_version() -> testresult::TestResult { let canonical_json = { // Deserialize and reserialize to have a consistent JSON format let json = std::fs::read_to_string("../../output/schema/schema-no-generics.json")?; let model: IndexedModel = serde_json::from_str(&json)?; serde_json::to_string_pretty(&model)? }; let schema_json = std::fs::read_to_string("../../output/schema/schema.json")?; let model: IndexedModel = serde_json::from_str(&schema_json)?; let model = expand(model, ExpandConfig::default())?; let json_no_generics = serde_json::to_string_pretty(&model)?; if canonical_json != json_no_generics { std::fs::create_dir_all("test-output")?; let mut out = std::fs::File::create("test-output/schema-no-generics-canonical.json")?; out.write_all(canonical_json.as_bytes())?; let mut out = std::fs::File::create("test-output/schema-no-generics-new.json")?; out.write_all(json_no_generics.as_bytes())?; panic!("Output differs from the canonical version. Both were written to 'test-output'"); } Ok(()) } }