Skip to content

Commit 77fbdca

Browse files
swallezAnaethelion
authored andcommitted
Add unwrapping and inlining to expand-generics (#2504)
* Add unwrapping and inlining to expand-generics * Make linter happy * Add unwrapping and inlining to expand-generics (Rust edition) * Update OpenAPI spec with inlined WithNullValue --------- Co-authored-by: Laurent Saint-Félix <laurent.saintfelix@elastic.co> (cherry picked from commit 2f1902b)
1 parent 451a197 commit 77fbdca

File tree

10 files changed

+261
-83
lines changed

10 files changed

+261
-83
lines changed

compiler-rs/clients_schema/src/builtins.rs

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,30 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
use once_cell::sync::Lazy;
19-
2018
use crate::TypeName;
19+
use crate::type_name;
20+
21+
macro_rules! declare_type_name {
22+
($id:ident,$namespace:expr,$name:expr) => {
23+
pub const $id: TypeName = type_name!($namespace, $name);
24+
};
25+
}
26+
27+
declare_type_name!(STRING, "_builtins", "string");
28+
declare_type_name!(BOOLEAN, "_builtins", "boolean");
29+
declare_type_name!(OBJECT, "_builtins", "object");
30+
declare_type_name!(BINARY, "_builtins", "binary");
31+
declare_type_name!(VOID, "_builtins", "void");
32+
declare_type_name!(NUMBER, "_builtins", "number");
33+
declare_type_name!(BYTE, "_builtins", "byte");
34+
declare_type_name!(INTEGER, "_builtins", "integer");
35+
declare_type_name!(LONG, "_builtins", "long");
36+
declare_type_name!(FLOAT, "_builtins", "float");
37+
declare_type_name!(DOUBLE, "_builtins", "double");
38+
declare_type_name!(NULL, "_builtins", "null");
39+
declare_type_name!(DICTIONARY, "_builtins", "Dictionary");
40+
declare_type_name!(USER_DEFINED, "_builtins", "UserDefined");
2141

22-
pub static STRING: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "string"));
23-
pub static BOOLEAN: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "boolean"));
24-
pub static OBJECT: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "object"));
25-
pub static BINARY: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "binary"));
26-
pub static VOID: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "void"));
27-
pub static NUMBER: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "number"));
28-
pub static BYTE: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "byte"));
29-
pub static INTEGER: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "integer"));
30-
pub static LONG: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "long"));
31-
pub static FLOAT: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "float"));
32-
pub static DOUBLE: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "double"));
33-
pub static NULL: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "null"));
34-
pub static DICTIONARY: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "Dictionary"));
35-
pub static USER_DEFINED: Lazy<TypeName> = Lazy::new(|| TypeName::new("_builtins", "UserDefined"));
42+
declare_type_name!(ADDITIONAL_PROPERTIES, "_spec_utils", "AdditionalProperties");
3643

37-
pub static ADDITIONAL_PROPERTIES: Lazy<TypeName> = Lazy::new(|| TypeName::new("_spec_utils", "AdditionalProperties"));
44+
declare_type_name!(WITH_NULL_VALUE, "_spec_utils", "WithNullValue");

compiler-rs/clients_schema/src/lib.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,17 @@ impl TypeName {
7373
}
7474
}
7575

76+
/// Creates a constant `TypeName` from static strings
77+
#[macro_export]
78+
macro_rules! type_name {
79+
($namespace:expr,$name:expr) => {
80+
TypeName {
81+
namespace: arcstr::literal!($namespace),
82+
name: arcstr::literal!($name),
83+
}
84+
};
85+
}
86+
7687
impl Display for TypeName {
7788
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
7889
write!(f, "{}:{}", self.namespace, self.name)
@@ -93,12 +104,6 @@ pub enum ValueOf {
93104
LiteralValue(LiteralValue),
94105
}
95106

96-
impl ValueOf {
97-
pub fn instance_of(name: TypeName) -> ValueOf {
98-
ValueOf::InstanceOf(InstanceOf::new(name))
99-
}
100-
}
101-
102107
impl From<TypeName> for ValueOf {
103108
fn from(name: TypeName) -> Self {
104109
ValueOf::InstanceOf(InstanceOf::new(name))

compiler-rs/clients_schema/src/transform/expand_generics.rs

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
use std::collections::HashMap;
18+
use std::collections::{HashMap, HashSet};
1919

2020
use anyhow::bail;
2121
use indexmap::IndexMap;
@@ -26,22 +26,43 @@ use crate::*;
2626
struct Ctx {
2727
new_types: IndexMap<TypeName, TypeDefinition>,
2828
types_seen: std::collections::HashSet<TypeName>,
29+
config: ExpandConfig,
2930
}
3031

3132
/// Generic parameters of a type
3233
type GenericParams = Vec<TypeName>;
33-
/// Generic arguments for an instanciated generic type
34+
/// Generic arguments for an instantiated generic type
3435
type GenericArgs = Vec<ValueOf>;
3536
/// Mapping from generic arguments to values
3637
type GenericMapping = HashMap<TypeName, ValueOf>;
3738

38-
/// Expand all generics by creating new concrete types for every instanciation of a generic type.
39+
#[derive(Clone, Debug)]
40+
pub struct ExpandConfig {
41+
/// Generic types that will be inlined by replacing them with their definition, propagating generic arguments.
42+
pub unwrap: HashSet<TypeName>,
43+
// Generic types that will be unwrapped by replacing them with their (single) generic parameter.
44+
pub inline: HashSet<TypeName>,
45+
}
46+
47+
impl Default for ExpandConfig {
48+
fn default() -> Self {
49+
ExpandConfig {
50+
unwrap: Default::default(),
51+
inline: HashSet::from([builtins::WITH_NULL_VALUE])
52+
}
53+
}
54+
}
55+
56+
/// Expand all generics by creating new concrete types for every instantiation of a generic type.
3957
///
4058
/// The resulting model has no generics anymore. Top-level generic parameters (e.g. SearchRequest's TDocument) are
4159
/// replaced by user_defined_data.
42-
pub fn expand_generics(model: IndexedModel) -> anyhow::Result<IndexedModel> {
60+
pub fn expand(model: IndexedModel, config: ExpandConfig) -> anyhow::Result<IndexedModel> {
4361
let mut model = model;
44-
let mut ctx = Ctx::default();
62+
let mut ctx = Ctx {
63+
config,
64+
..Ctx::default()
65+
};
4566

4667
for endpoint in &model.endpoints {
4768
for name in [&endpoint.request, &endpoint.response].into_iter().flatten() {
@@ -317,6 +338,14 @@ pub fn expand_generics(model: IndexedModel) -> anyhow::Result<IndexedModel> {
317338
return Ok(p.clone());
318339
}
319340

341+
// Inline or unwrap if required by the config
342+
if ctx.config.inline.contains(&inst.typ) {
343+
return inline_generic_type(inst, mappings, model, ctx);
344+
}
345+
if ctx.config.unwrap.contains(&inst.typ) {
346+
return unwrap_generic_type(inst, mappings, model, ctx);
347+
}
348+
320349
// Expand generic parameters, if any
321350
let args = inst
322351
.generics
@@ -346,6 +375,51 @@ pub fn expand_generics(model: IndexedModel) -> anyhow::Result<IndexedModel> {
346375
}
347376
}
348377

378+
/// Inlines a value of a generic type by replacing it with its definition, propagating
379+
/// generic arguments.
380+
fn inline_generic_type(
381+
value: &InstanceOf,
382+
_mappings: &GenericMapping,
383+
model: &IndexedModel,
384+
ctx: &mut Ctx,
385+
) -> anyhow::Result<ValueOf> {
386+
387+
// It has to be an alias (e.g. WithNullValue)
388+
if let TypeDefinition::TypeAlias(inline_def) = model.get_type(&value.typ)? {
389+
// Create mappings to resolve types in the inlined type's definition
390+
let mut inline_mappings = GenericMapping::new();
391+
for (source, dest) in inline_def.generics.iter().zip(value.generics.iter()) {
392+
inline_mappings.insert(source.clone(), dest.clone());
393+
}
394+
// and expand the inlined type's alias definition
395+
let result = expand_valueof(&inline_def.typ, &inline_mappings, model, ctx)?;
396+
return Ok(result);
397+
} else {
398+
bail!("Expecting inlined type {} to be an alias", &value.typ);
399+
}
400+
}
401+
402+
/// Unwraps a value of a generic type by replacing it with its generic parameter
403+
fn unwrap_generic_type(
404+
value: &InstanceOf,
405+
mappings: &GenericMapping,
406+
model: &IndexedModel,
407+
ctx: &mut Ctx,
408+
) -> anyhow::Result<ValueOf> {
409+
410+
// It has to be an alias (e.g. Stringified)
411+
if let TypeDefinition::TypeAlias(_unwrap_def) = model.get_type(&value.typ)? {
412+
// Expand the inlined type's generic argument (there must be exactly one)
413+
if value.generics.len() != 1 {
414+
bail!("Expecting unwrapped type {} to have exactly one generic parameter", &value.typ);
415+
}
416+
let result = expand_valueof(&value.generics[0], mappings, model, ctx)?;
417+
return Ok(result);
418+
} else {
419+
bail!("Expecting unwrapped type {} to be an alias", &value.typ);
420+
}
421+
}
422+
349423
//---------------------------------------------------------------------------------------------
350424
// Misc
351425
//---------------------------------------------------------------------------------------------
@@ -422,12 +496,12 @@ mod tests {
422496

423497
let schema_json = std::fs::read_to_string("../../output/schema/schema.json")?;
424498
let model: IndexedModel = serde_json::from_str(&schema_json)?;
425-
let model = expand_generics(model)?;
499+
let model = expand(model, ExpandConfig::default())?;
426500

427501
let json_no_generics = serde_json::to_string_pretty(&model)?;
428502

429503
if canonical_json != json_no_generics {
430-
std::fs::create_dir("test-output")?;
504+
std::fs::create_dir_all("test-output")?;
431505
let mut out = std::fs::File::create("test-output/schema-no-generics-canonical.json")?;
432506
out.write_all(canonical_json.as_bytes())?;
433507

compiler-rs/clients_schema/src/transform/mod.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,13 @@
1717

1818
//! Utilities to transform API models and common transformations:
1919
//! * filtering according to availability
20+
//! * expand generic types so that the model doesn't contain generic types anymore
2021
2122
mod availability;
2223
mod expand_generics;
2324

2425
use std::collections::HashSet;
2526

26-
use availability::Availability;
27-
2827
use crate::{Availabilities, IndexedModel, TypeName};
2928

3029
/// The working state of a type graph traversal algorithm. It keeps track of the types that
@@ -67,6 +66,7 @@ impl Iterator for Worksheet {
6766
}
6867
}
6968

69+
pub use availability::Availability;
7070
/// Transform a model to only keep the endpoints and types that match a predicate on the `availability`
7171
/// properties.
7272
pub fn filter_availability(
@@ -76,6 +76,8 @@ pub fn filter_availability(
7676
Availability::filter(model, avail_filter)
7777
}
7878

79-
pub fn expand_generics(model: IndexedModel) -> anyhow::Result<IndexedModel> {
80-
expand_generics::expand_generics(model)
79+
80+
pub use expand_generics::ExpandConfig;
81+
pub fn expand_generics(model: IndexedModel, config: ExpandConfig) -> anyhow::Result<IndexedModel> {
82+
expand_generics::expand(model, config)
8183
}

compiler-rs/clients_schema_to_openapi/src/main.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,10 @@ impl Cli {
8787
},
8888
};
8989

90-
model = clients_schema::transform::expand_generics(model)?;
91-
model = clients_schema::transform::filter_availability(model, filter)?;
90+
use clients_schema::transform::*;
91+
92+
model = expand_generics(model, ExpandConfig::default())?;
93+
model = filter_availability(model, filter)?;
9294
}
9395
}
9496

Binary file not shown.

compiler-rs/compiler-wasm-lib/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use anyhow::bail;
1919
use clients_schema::{Availabilities, Visibility};
2020
use wasm_bindgen::prelude::*;
21+
use clients_schema::transform::ExpandConfig;
2122

2223
#[cfg(all(not(target_arch = "wasm32"), not(feature = "cargo-clippy")))]
2324
compile_error!("To build this crate use `make compiler-wasm-lib`");
@@ -43,7 +44,7 @@ fn convert0(json: &str, flavor: &str) -> anyhow::Result<String> {
4344
};
4445

4546
let mut schema = clients_schema::IndexedModel::from_reader(json.as_bytes())?;
46-
schema = clients_schema::transform::expand_generics(schema)?;
47+
schema = clients_schema::transform::expand_generics(schema, ExpandConfig::default())?;
4748
if let Some(filter) = filter {
4849
schema = clients_schema::transform::filter_availability(schema, filter)?;
4950
}

compiler-rs/openapi_to_clients_schema/src/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ fn generate_dictionary_def(
360360
typ: ValueOf::InstanceOf(InstanceOf {
361361
typ: builtins::DICTIONARY.clone(),
362362
generics: vec![
363-
ValueOf::instance_of(builtins::STRING.clone()),
363+
ValueOf::from(builtins::STRING.clone()),
364364
match value {
365365
AdditionalProperties::Any(_) => (&builtins::USER_DEFINED).into(),
366366

compiler/src/transform/expand-generics.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,41 @@ import { sortTypeDefinitions } from '../model/utils'
3535
import { argv } from 'zx'
3636
import { join } from 'path'
3737

38+
export class ExpansionConfig {
39+
unwrappedTypes?: TypeName[] | string[]
40+
inlinedTypes?: TypeName[] | string[]
41+
}
42+
3843
/**
39-
* Expand all generics by creating new concrete types for every instanciation of a generic type.
44+
* Expand all generics by creating new concrete types for every instantiation of a generic type.
4045
*
4146
* The resulting model has no generics anymore. Top-level generic parameters (e.g. SearchRequest's TDocument) are
4247
* replaced by user_defined_data.
4348
*
4449
* @param inputModel the input model
50+
* @param unwrappedTypes types that should not be expanded but unwrapped as their generic parameter.
4551
* @return a new model with generics expanded
4652
*/
47-
export function expandGenerics (inputModel: Model): Model {
53+
export function expandGenerics (inputModel: Model, config?: ExpansionConfig): Model {
54+
const typesToUnwrap = new Set<string>()
55+
const typesToInline: Set<string> = new Set<string>()
56+
57+
for (const name of config?.unwrappedTypes ?? []) {
58+
if (typeof name === 'string') {
59+
typesToUnwrap.add(name)
60+
} else {
61+
typesToUnwrap.add(nameKey(name))
62+
}
63+
}
64+
65+
for (const name of config?.inlinedTypes ?? []) {
66+
if (typeof name === 'string') {
67+
typesToInline.add(name)
68+
} else {
69+
typesToInline.add(nameKey(name))
70+
}
71+
}
72+
4873
const typesSeen = new Set<string>()
4974

5075
const types = new Array<TypeDefinition>()
@@ -338,6 +363,35 @@ export function expandGenerics (inputModel: Model): Model {
338363
}
339364

340365
case 'instance_of': {
366+
const valueOfType = nameKey(value.type)
367+
368+
// If this is a type that has to be unwrapped, return its generic parameter's type
369+
if (typesToUnwrap.has(valueOfType)) {
370+
// @ts-expect-error
371+
const x = value.generics[0]
372+
return expandValueOf(x, mappings)
373+
}
374+
375+
// If this is a type that has to be inlined
376+
if (typesToInline.has(valueOfType)) {
377+
// It has to be an alias (e.g. Stringified or WithNullValue
378+
const inlinedTypeDef = inputTypeByName.get(valueOfType)
379+
if (inlinedTypeDef?.kind !== 'type_alias') {
380+
throw Error(`Inlined type ${valueOfType} should be an alias definition`)
381+
}
382+
383+
const inlineMappings = new Map<string, ValueOf>()
384+
for (let i = 0; i < (inlinedTypeDef.generics?.length ?? 0); i++) {
385+
// @ts-expect-error
386+
const source = inlinedTypeDef.generics[i]
387+
// @ts-expect-error
388+
const dest = value.generics[i]
389+
inlineMappings.set(nameKey(source), dest)
390+
}
391+
392+
return expandValueOf(inlinedTypeDef.type, inlineMappings)
393+
}
394+
341395
// If this is a generic parameter, return its mapping
342396
const mapping = mappings.get(nameKey(value.type))
343397
if (mapping !== undefined) {
@@ -467,7 +521,10 @@ async function expandGenericsFromFile (inPath: string, outPath: string): Promise
467521
)
468522

469523
const inputModel = JSON.parse(inputText)
470-
const outputModel = expandGenerics(inputModel)
524+
const outputModel = expandGenerics(inputModel, {
525+
// unwrappedTypes: ["_spec_utils:Stringified"],
526+
inlinedTypes: ['_spec_utils:WithNullValue']
527+
})
471528

472529
await writeFile(
473530
outPath,

0 commit comments

Comments
 (0)