diff --git a/compiler-rs/clients_schema/src/builtins.rs b/compiler-rs/clients_schema/src/builtins.rs index 85b288a0e2..8ec5dcfc99 100644 --- a/compiler-rs/clients_schema/src/builtins.rs +++ b/compiler-rs/clients_schema/src/builtins.rs @@ -15,23 +15,30 @@ // specific language governing permissions and limitations // under the License. -use once_cell::sync::Lazy; - use crate::TypeName; +use crate::type_name; + +macro_rules! declare_type_name { + ($id:ident,$namespace:expr,$name:expr) => { + pub const $id: TypeName = type_name!($namespace, $name); + }; +} + +declare_type_name!(STRING, "_builtins", "string"); +declare_type_name!(BOOLEAN, "_builtins", "boolean"); +declare_type_name!(OBJECT, "_builtins", "object"); +declare_type_name!(BINARY, "_builtins", "binary"); +declare_type_name!(VOID, "_builtins", "void"); +declare_type_name!(NUMBER, "_builtins", "number"); +declare_type_name!(BYTE, "_builtins", "byte"); +declare_type_name!(INTEGER, "_builtins", "integer"); +declare_type_name!(LONG, "_builtins", "long"); +declare_type_name!(FLOAT, "_builtins", "float"); +declare_type_name!(DOUBLE, "_builtins", "double"); +declare_type_name!(NULL, "_builtins", "null"); +declare_type_name!(DICTIONARY, "_builtins", "Dictionary"); +declare_type_name!(USER_DEFINED, "_builtins", "UserDefined"); -pub static STRING: Lazy = Lazy::new(|| TypeName::new("_builtins", "string")); -pub static BOOLEAN: Lazy = Lazy::new(|| TypeName::new("_builtins", "boolean")); -pub static OBJECT: Lazy = Lazy::new(|| TypeName::new("_builtins", "object")); -pub static BINARY: Lazy = Lazy::new(|| TypeName::new("_builtins", "binary")); -pub static VOID: Lazy = Lazy::new(|| TypeName::new("_builtins", "void")); -pub static NUMBER: Lazy = Lazy::new(|| TypeName::new("_builtins", "number")); -pub static BYTE: Lazy = Lazy::new(|| TypeName::new("_builtins", "byte")); -pub static INTEGER: Lazy = Lazy::new(|| TypeName::new("_builtins", "integer")); -pub static LONG: Lazy = Lazy::new(|| TypeName::new("_builtins", "long")); -pub static FLOAT: Lazy = Lazy::new(|| TypeName::new("_builtins", "float")); -pub static DOUBLE: Lazy = Lazy::new(|| TypeName::new("_builtins", "double")); -pub static NULL: Lazy = Lazy::new(|| TypeName::new("_builtins", "null")); -pub static DICTIONARY: Lazy = Lazy::new(|| TypeName::new("_builtins", "Dictionary")); -pub static USER_DEFINED: Lazy = Lazy::new(|| TypeName::new("_builtins", "UserDefined")); +declare_type_name!(ADDITIONAL_PROPERTIES, "_spec_utils", "AdditionalProperties"); -pub static ADDITIONAL_PROPERTIES: Lazy = Lazy::new(|| TypeName::new("_spec_utils", "AdditionalProperties")); +declare_type_name!(WITH_NULL_VALUE, "_spec_utils", "WithNullValue"); diff --git a/compiler-rs/clients_schema/src/lib.rs b/compiler-rs/clients_schema/src/lib.rs index f771442881..4ec7dd66d9 100644 --- a/compiler-rs/clients_schema/src/lib.rs +++ b/compiler-rs/clients_schema/src/lib.rs @@ -73,6 +73,17 @@ impl TypeName { } } +/// Creates a constant `TypeName` from static strings +#[macro_export] +macro_rules! type_name { + ($namespace:expr,$name:expr) => { + TypeName { + namespace: arcstr::literal!($namespace), + name: arcstr::literal!($name), + } + }; +} + impl Display for TypeName { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}:{}", self.namespace, self.name) @@ -93,12 +104,6 @@ pub enum ValueOf { LiteralValue(LiteralValue), } -impl ValueOf { - pub fn instance_of(name: TypeName) -> ValueOf { - ValueOf::InstanceOf(InstanceOf::new(name)) - } -} - impl From for ValueOf { fn from(name: TypeName) -> Self { ValueOf::InstanceOf(InstanceOf::new(name)) diff --git a/compiler-rs/clients_schema/src/transform/expand_generics.rs b/compiler-rs/clients_schema/src/transform/expand_generics.rs index 53aed06437..9d7da2be4e 100644 --- a/compiler-rs/clients_schema/src/transform/expand_generics.rs +++ b/compiler-rs/clients_schema/src/transform/expand_generics.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use anyhow::bail; use indexmap::IndexMap; @@ -26,22 +26,43 @@ use crate::*; struct Ctx { new_types: IndexMap, types_seen: std::collections::HashSet, + config: ExpandConfig, } /// Generic parameters of a type type GenericParams = Vec; -/// Generic arguments for an instanciated generic type +/// Generic arguments for an instantiated generic type type GenericArgs = Vec; /// Mapping from generic arguments to values type GenericMapping = HashMap; -/// Expand all generics by creating new concrete types for every instanciation of a generic type. +#[derive(Clone, Debug)] +pub struct ExpandConfig { + /// Generic types that will be inlined by replacing them with their definition, propagating generic arguments. + pub unwrap: HashSet, + // Generic types that will be unwrapped by replacing them with their (single) generic parameter. + pub inline: HashSet, +} + +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_generics(model: IndexedModel) -> anyhow::Result { +pub fn expand(model: IndexedModel, config: ExpandConfig) -> anyhow::Result { let mut model = model; - let mut ctx = Ctx::default(); + let mut ctx = Ctx { + config, + ..Ctx::default() + }; for endpoint in &model.endpoints { for name in [&endpoint.request, &endpoint.response].into_iter().flatten() { @@ -317,6 +338,14 @@ pub fn expand_generics(model: IndexedModel) -> anyhow::Result { 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 @@ -346,6 +375,51 @@ pub fn expand_generics(model: IndexedModel) -> anyhow::Result { } } + /// 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 { + + // 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 { + + // 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 //--------------------------------------------------------------------------------------------- @@ -422,12 +496,12 @@ mod tests { let schema_json = std::fs::read_to_string("../../output/schema/schema.json")?; let model: IndexedModel = serde_json::from_str(&schema_json)?; - let model = expand_generics(model)?; + 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("test-output")?; + 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())?; diff --git a/compiler-rs/clients_schema/src/transform/mod.rs b/compiler-rs/clients_schema/src/transform/mod.rs index 1a5feeecc3..49ff66ce02 100644 --- a/compiler-rs/clients_schema/src/transform/mod.rs +++ b/compiler-rs/clients_schema/src/transform/mod.rs @@ -17,14 +17,13 @@ //! Utilities to transform API models and common transformations: //! * filtering according to availability +//! * expand generic types so that the model doesn't contain generic types anymore mod availability; mod expand_generics; use std::collections::HashSet; -use availability::Availability; - use crate::{Availabilities, IndexedModel, TypeName}; /// The working state of a type graph traversal algorithm. It keeps track of the types that @@ -67,6 +66,7 @@ impl Iterator for Worksheet { } } +pub use availability::Availability; /// Transform a model to only keep the endpoints and types that match a predicate on the `availability` /// properties. pub fn filter_availability( @@ -76,6 +76,8 @@ pub fn filter_availability( Availability::filter(model, avail_filter) } -pub fn expand_generics(model: IndexedModel) -> anyhow::Result { - expand_generics::expand_generics(model) + +pub use expand_generics::ExpandConfig; +pub fn expand_generics(model: IndexedModel, config: ExpandConfig) -> anyhow::Result { + expand_generics::expand(model, config) } diff --git a/compiler-rs/clients_schema_to_openapi/src/main.rs b/compiler-rs/clients_schema_to_openapi/src/main.rs index 06234875f1..8601c79e75 100644 --- a/compiler-rs/clients_schema_to_openapi/src/main.rs +++ b/compiler-rs/clients_schema_to_openapi/src/main.rs @@ -87,8 +87,10 @@ impl Cli { }, }; - model = clients_schema::transform::expand_generics(model)?; - model = clients_schema::transform::filter_availability(model, filter)?; + use clients_schema::transform::*; + + model = expand_generics(model, ExpandConfig::default())?; + model = filter_availability(model, filter)?; } } diff --git a/compiler-rs/compiler-wasm-lib/pkg/compiler_wasm_lib_bg.wasm b/compiler-rs/compiler-wasm-lib/pkg/compiler_wasm_lib_bg.wasm index 6ce81c1721..eca2228486 100644 Binary files a/compiler-rs/compiler-wasm-lib/pkg/compiler_wasm_lib_bg.wasm and b/compiler-rs/compiler-wasm-lib/pkg/compiler_wasm_lib_bg.wasm differ diff --git a/compiler-rs/compiler-wasm-lib/src/lib.rs b/compiler-rs/compiler-wasm-lib/src/lib.rs index 2bf0c5c8f5..33f0fac3c9 100644 --- a/compiler-rs/compiler-wasm-lib/src/lib.rs +++ b/compiler-rs/compiler-wasm-lib/src/lib.rs @@ -18,6 +18,7 @@ use anyhow::bail; use clients_schema::{Availabilities, Visibility}; use wasm_bindgen::prelude::*; +use clients_schema::transform::ExpandConfig; #[cfg(all(not(target_arch = "wasm32"), not(feature = "cargo-clippy")))] compile_error!("To build this crate use `make compiler-wasm-lib`"); @@ -43,7 +44,7 @@ fn convert0(json: &str, flavor: &str) -> anyhow::Result { }; let mut schema = clients_schema::IndexedModel::from_reader(json.as_bytes())?; - schema = clients_schema::transform::expand_generics(schema)?; + schema = clients_schema::transform::expand_generics(schema, ExpandConfig::default())?; if let Some(filter) = filter { schema = clients_schema::transform::filter_availability(schema, filter)?; } diff --git a/compiler-rs/openapi_to_clients_schema/src/types.rs b/compiler-rs/openapi_to_clients_schema/src/types.rs index 42354dc41d..89b5ea7615 100644 --- a/compiler-rs/openapi_to_clients_schema/src/types.rs +++ b/compiler-rs/openapi_to_clients_schema/src/types.rs @@ -360,7 +360,7 @@ fn generate_dictionary_def( typ: ValueOf::InstanceOf(InstanceOf { typ: builtins::DICTIONARY.clone(), generics: vec![ - ValueOf::instance_of(builtins::STRING.clone()), + ValueOf::from(builtins::STRING.clone()), match value { AdditionalProperties::Any(_) => (&builtins::USER_DEFINED).into(), diff --git a/compiler/src/transform/expand-generics.ts b/compiler/src/transform/expand-generics.ts index 07378e9c51..348cc9b365 100644 --- a/compiler/src/transform/expand-generics.ts +++ b/compiler/src/transform/expand-generics.ts @@ -35,16 +35,41 @@ import { sortTypeDefinitions } from '../model/utils' import { argv } from 'zx' import { join } from 'path' +export class ExpansionConfig { + unwrappedTypes?: TypeName[] | string[] + inlinedTypes?: TypeName[] | string[] +} + /** - * Expand all generics by creating new concrete types for every instanciation of a generic type. + * 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. * * @param inputModel the input model + * @param unwrappedTypes types that should not be expanded but unwrapped as their generic parameter. * @return a new model with generics expanded */ -export function expandGenerics (inputModel: Model): Model { +export function expandGenerics (inputModel: Model, config?: ExpansionConfig): Model { + const typesToUnwrap = new Set() + const typesToInline: Set = new Set() + + for (const name of config?.unwrappedTypes ?? []) { + if (typeof name === 'string') { + typesToUnwrap.add(name) + } else { + typesToUnwrap.add(nameKey(name)) + } + } + + for (const name of config?.inlinedTypes ?? []) { + if (typeof name === 'string') { + typesToInline.add(name) + } else { + typesToInline.add(nameKey(name)) + } + } + const typesSeen = new Set() const types = new Array() @@ -338,6 +363,35 @@ export function expandGenerics (inputModel: Model): Model { } case 'instance_of': { + const valueOfType = nameKey(value.type) + + // If this is a type that has to be unwrapped, return its generic parameter's type + if (typesToUnwrap.has(valueOfType)) { + // @ts-expect-error + const x = value.generics[0] + return expandValueOf(x, mappings) + } + + // If this is a type that has to be inlined + if (typesToInline.has(valueOfType)) { + // It has to be an alias (e.g. Stringified or WithNullValue + const inlinedTypeDef = inputTypeByName.get(valueOfType) + if (inlinedTypeDef?.kind !== 'type_alias') { + throw Error(`Inlined type ${valueOfType} should be an alias definition`) + } + + const inlineMappings = new Map() + for (let i = 0; i < (inlinedTypeDef.generics?.length ?? 0); i++) { + // @ts-expect-error + const source = inlinedTypeDef.generics[i] + // @ts-expect-error + const dest = value.generics[i] + inlineMappings.set(nameKey(source), dest) + } + + return expandValueOf(inlinedTypeDef.type, inlineMappings) + } + // If this is a generic parameter, return its mapping const mapping = mappings.get(nameKey(value.type)) if (mapping !== undefined) { @@ -467,7 +521,10 @@ async function expandGenericsFromFile (inPath: string, outPath: string): Promise ) const inputModel = JSON.parse(inputText) - const outputModel = expandGenerics(inputModel) + const outputModel = expandGenerics(inputModel, { + // unwrappedTypes: ["_spec_utils:Stringified"], + inlinedTypes: ['_spec_utils:WithNullValue'] + }) await writeFile( outPath, diff --git a/output/openapi/elasticsearch-serverless-openapi.json b/output/openapi/elasticsearch-serverless-openapi.json index 60f5ba0c07..0c24bbe744 100644 --- a/output/openapi/elasticsearch-serverless-openapi.json +++ b/output/openapi/elasticsearch-serverless-openapi.json @@ -1926,7 +1926,14 @@ "type": "string" }, "index_name": { - "$ref": "#/components/schemas/_spec_utils:WithNullValueIndexName" + "oneOf": [ + { + "$ref": "#/components/schemas/_types:IndexName" + }, + { + "$ref": "#/components/schemas/_spec_utils:NullValue" + } + ] }, "is_native": { "type": "boolean" @@ -2049,7 +2056,14 @@ "type": "object", "properties": { "last_access_control_sync_error": { - "$ref": "#/components/schemas/_spec_utils:WithNullValuestring" + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/_spec_utils:NullValue" + } + ] }, "last_access_control_sync_scheduled_at": { "$ref": "#/components/schemas/_types:DateTime" @@ -2067,10 +2081,24 @@ "type": "number" }, "last_seen": { - "$ref": "#/components/schemas/_spec_utils:WithNullValueDateTime" + "oneOf": [ + { + "$ref": "#/components/schemas/_types:DateTime" + }, + { + "$ref": "#/components/schemas/_spec_utils:NullValue" + } + ] }, "last_sync_error": { - "$ref": "#/components/schemas/_spec_utils:WithNullValuestring" + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/_spec_utils:NullValue" + } + ] }, "last_sync_scheduled_at": { "$ref": "#/components/schemas/_types:DateTime" @@ -2230,7 +2258,14 @@ "type": "string" }, "index_name": { - "$ref": "#/components/schemas/_spec_utils:WithNullValueIndexName" + "oneOf": [ + { + "$ref": "#/components/schemas/_types:IndexName" + }, + { + "$ref": "#/components/schemas/_spec_utils:NullValue" + } + ] }, "is_native": { "type": "boolean" @@ -2622,10 +2657,24 @@ "type": "object", "properties": { "api_key_id": { - "$ref": "#/components/schemas/_spec_utils:WithNullValuestring" + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/_spec_utils:NullValue" + } + ] }, "api_key_secret_id": { - "$ref": "#/components/schemas/_spec_utils:WithNullValuestring" + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/_spec_utils:NullValue" + } + ] } } } @@ -2753,7 +2802,14 @@ "type": "object", "properties": { "error": { - "$ref": "#/components/schemas/_spec_utils:WithNullValuestring" + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/_spec_utils:NullValue" + } + ] } }, "required": [ @@ -2954,7 +3010,14 @@ "type": "object", "properties": { "index_name": { - "$ref": "#/components/schemas/_spec_utils:WithNullValueIndexName" + "oneOf": [ + { + "$ref": "#/components/schemas/_types:IndexName" + }, + { + "$ref": "#/components/schemas/_spec_utils:NullValue" + } + ] } }, "required": [ @@ -50394,44 +50457,11 @@ "error" ] }, - "_spec_utils:WithNullValuestring": { - "description": "`WithNullValue` allows for explicit null assignments in contexts where `null` should be interpreted as an\nactual value.", - "oneOf": [ - { - "type": "string" - }, - { - "$ref": "#/components/schemas/_spec_utils:NullValue" - } - ] - }, "_spec_utils:NullValue": { "nullable": true, "description": "A `null` value that is to be interpreted as an actual value, unless other uses of `null` that are equivalent\nto a missing value. It is used for exemple in settings, where using the `NullValue` for a setting will reset\nit to its default value.", "type": "string" }, - "_spec_utils:WithNullValueDateTime": { - "description": "`WithNullValue` allows for explicit null assignments in contexts where `null` should be interpreted as an\nactual value.", - "oneOf": [ - { - "$ref": "#/components/schemas/_types:DateTime" - }, - { - "$ref": "#/components/schemas/_spec_utils:NullValue" - } - ] - }, - "_spec_utils:WithNullValueIndexName": { - "description": "`WithNullValue` allows for explicit null assignments in contexts where `null` should be interpreted as an\nactual value.", - "oneOf": [ - { - "$ref": "#/components/schemas/_types:IndexName" - }, - { - "$ref": "#/components/schemas/_spec_utils:NullValue" - } - ] - }, "connector._types:ConnectorSyncJob": { "type": "object", "properties": {