Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 136 additions & 48 deletions lib/codegen/fromcto/rust/rustvisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ class RustVisitor {

classDeclaration.getProperties().forEach((property) => {
parameters.fileWriter.writeLine(1, '');
property.accept(this, parameters);
property.accept(this, { ...parameters, emittingClass: classDeclaration });
});

parameters.fileWriter.writeLine(0, '}');
Expand All @@ -334,14 +334,7 @@ class RustVisitor {
// Only generate unions when explicitly requested via flattenSubclassesToUnion parameter
// and when all subclasses are also in the current model file
if (parameters.flattenSubclassesToUnion) {
const subclasses = classDeclaration.getDirectSubclasses?.();
const currentModelFile = classDeclaration.getModelFile();

// Filter subclasses to only include those in the same model file
const localSubclasses =
subclasses?.filter(
(subclass) => subclass.getModelFile() === currentModelFile
) || [];
const localSubclasses = this.unionSubclasses(classDeclaration);

if (localSubclasses.length > 0) {
const unionName = `${classDeclaration.getName()}Union`;
Expand Down Expand Up @@ -369,9 +362,7 @@ class RustVisitor {
parameters.fileWriter.writeLine(0, '');
}

// Add each subclass (filter out enum subclasses as they're not relevant for unions)
localSubclasses
.filter((subclass) => !subclass.isEnum?.())
.forEach((subclass) => {
const subclassName = subclass.getName();
const fullyQualifiedName =
Expand Down Expand Up @@ -434,42 +425,13 @@ class RustVisitor {
}
}

// Determine if we should use union types
let useUnion = false;

// Check for @union decorator
const hasUnion = decorators.some(
(decorator) => decorator.getName() === 'union'
);

// Check if this field's type has subclasses and we should flatten to union
if (!field.isPrimitive?.() && parameters.flattenSubclassesToUnion) {
try {
const fieldType = field
.getParent()
.getModelFile()
.getModelManager()
.getType(field.getFullyQualifiedTypeName());
const subclasses = fieldType.getDirectSubclasses?.();
if (subclasses && subclasses.length > 0) {
const isEnumRef = fieldType.isEnum?.();
const isMapRef = fieldType.isMapDeclaration?.();
useUnion = !(isEnumRef || isMapRef);
}
} catch (e) {
// If we can't resolve the type, don't use union
useUnion = false;
}
}

// Override with explicit @union decorator
if (hasUnion) {
useUnion = true;
}
const useUnion = this.usesUnion(field, parameters.flattenSubclassesToUnion);

let type = this.toRustType(field.type, useUnion);
if (field.isArray?.()) {
type = `Vec<${type}>`;
} else if (this.isRecursive(field, parameters.flattenSubclassesToUnion, parameters.emittingClass)) {
type = `Box<${type}>`;
}

// Handle HashMap fields separately with full DateTime serialization support
Expand Down Expand Up @@ -729,11 +691,8 @@ class RustVisitor {
* @private
*/
visitMapDeclaration(mapDeclaration, parameters) {
const mapKeyType = mapDeclaration.getKey().getType();
const mapValueType = mapDeclaration.getValue().getType();

const keyType = this.toRustType(mapKeyType);
const valueType = this.toRustType(mapValueType);
const keyType = this.toRustMapEntryType(mapDeclaration, mapDeclaration.getKey());
const valueType = this.toRustMapEntryType(mapDeclaration, mapDeclaration.getValue());

parameters.fileWriter.writeLine(
0,
Expand All @@ -744,6 +703,133 @@ class RustVisitor {
return null;
}

/**
* Resolves the Rust type of a map key or value, unwrapping scalars
* to their underlying primitive since scalars are not emitted.
* @param {MapDeclaration} mapDeclaration - the owning map declaration
* @param {Object} entry - the map key or value type
* @return {string} the corresponding type in Rust
* @private
*/
toRustMapEntryType(mapDeclaration, entry) {
const type = entry.getType();
if (!ModelUtil.isPrimitiveType(type)) {
const declaration = mapDeclaration.getModelFile().getType(type);
if (declaration?.isScalarDeclaration?.()) {
return this.toRustType(declaration.getType());
}
}
return this.toRustType(type);
}

/**
* Lists the subclasses that become variants of a declaration's union enum.
* Subclasses from other model files are left out because the enum is
* written into the declaring file, which cannot reference them.
* @param {Declaration} declaration - the declaration that may have a union
* @return {Declaration[]} the subclasses that are union variants
* @private
*/
unionSubclasses(declaration) {
if (declaration?.isEnum?.() || declaration?.isMapDeclaration?.()) {
return [];
}
return (declaration?.getDirectSubclasses?.() ?? []).filter(
(subclass) =>
subclass.getModelFile() === declaration.getModelFile() &&
!subclass.isEnum?.()
);
}

/**
* Determines whether a property is emitted as a `<Type>Union` enum.
* Relationships never are: they are written as the plain named type.
* @param {Property} property - the property to check
* @param {boolean} [flatten] - the flattenSubclassesToUnion parameter
* @return {boolean} true if the property type is a union enum
* @private
*/
usesUnion(property, flatten = false) {
if (property.isRelationship?.()) {
return false;
}
const hasUnionDecorator = property
.getDecorators?.()
?.some((decorator) => decorator.getName() === 'union');
if (hasUnionDecorator) {
return true;
}
if (!flatten || property.isPrimitive?.()) {
return false;
}
try {
const target = property
.getParent()
.getModelFile()
.getModelManager()
.getType(property.getFullyQualifiedTypeName());
return this.unionSubclasses(target).length > 0;
} catch (e) {
return false;
}
}

/**
* Determines whether a non-array property refers back to its declaring
* class, directly or through other classes, which makes the Rust struct
* infinitely sized unless the property is boxed. Every traversed property
* follows the same union rules as the generated field, because a union
* enum holds a variant for each local subclass, and for the type itself
* unless it is abstract. The walk is iterative so deep models cannot
* exhaust the call stack.
* @param {Property} property - the property to check
* @param {boolean} [flatten] - the flattenSubclassesToUnion parameter
* @param {ClassDeclaration} [emittingClass] - the class whose struct holds
* the property; inherited properties are written into every subclass, so
* this can differ from the property's declaring class
* @return {boolean} true if the property type reaches the emitting class
* @private
*/
isRecursive(property, flatten = false, emittingClass = undefined) {
const owner = emittingClass ?? property.getParent?.();
const modelManager = owner?.getModelFile?.().getModelManager?.();
const ownerName = owner?.getFullyQualifiedName?.();
if (!ownerName || !modelManager || property.isPrimitive?.()) {
return false;
}
const typesReachedBy = (p) => {
const typeName = p.getFullyQualifiedTypeName();
if (!this.usesUnion(p, flatten)) {
return [typeName];
}
const target = modelManager.getType(typeName);
const variants = this.unionSubclasses(target)
.map((sub) => sub.getFullyQualifiedName());
return target?.isAbstract?.() ? variants : [typeName, ...variants];
};
const seen = new Set();
const pending = typesReachedBy(property);
Comment on lines +810 to +811
while (pending.length > 0) {
const typeName = pending.pop();
if (typeName === ownerName) {
return true;
}
if (seen.has(typeName)) {
continue;
}
seen.add(typeName);
const declaration = modelManager.getType(typeName);
if (!declaration?.isClassDeclaration?.() || declaration.isEnum?.()) {
continue;
}
declaration
.getProperties()
.filter((p) => !p.isArray() && !p.isPrimitive())
.forEach((p) => pending.push(...typesReachedBy(p)));
}
return false;
}

/**
* Visitor design pattern
* @param {RelationshipDeclaration} relationshipDeclaration - the object being visited
Expand All @@ -755,6 +841,8 @@ class RustVisitor {
let type = relationshipDeclaration.type;
if (relationshipDeclaration.isArray?.()) {
type = `Vec<${type}>`;
} else if (this.isRecursive(relationshipDeclaration, false, parameters.emittingClass)) {
type = `Box<${type}>`;
Comment thread
Ani07-05 marked this conversation as resolved.
Resolved
Comment thread
Ani07-05 marked this conversation as resolved.
Resolved
}
if (relationshipDeclaration.isOptional?.()) {
type = `Option<${type}>`;
Expand Down
12 changes: 6 additions & 6 deletions test/codegen/__snapshots__/codegen.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6083,7 +6083,7 @@ pub enum TShirtSizeType {
LARGE,
}

pub type EmployeeTShirtSizes = HashMap<SSN, TShirtSizeType>;
pub type EmployeeTShirtSizes = HashMap<String, TShirtSizeType>;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Address {
Expand Down Expand Up @@ -6172,15 +6172,15 @@ pub struct Info {

pub type CompanyProperties = HashMap<String, String>;

pub type EmployeeLoginTimes = HashMap<String, Time>;
pub type EmployeeLoginTimes = HashMap<String, DateTime<Utc>>;

pub type EmployeeSocialSecurityNumbers = HashMap<String, SSN>;
pub type EmployeeSocialSecurityNumbers = HashMap<String, String>;

pub type NextOfKin = HashMap<KinName, KinTelephone>;
pub type NextOfKin = HashMap<String, String>;

pub type EmployeeProfiles = HashMap<String, Concept>;

pub type EmployeeDirectory = HashMap<SSN, Employee>;
pub type EmployeeDirectory = HashMap<String, Employee>;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Company {
Expand Down Expand Up @@ -6582,7 +6582,7 @@ pub struct Manager {
rename = "manager",
skip_serializing_if = "Option::is_none",
)]
pub manager: Option<Manager>,
pub manager: Option<Box<Manager>>,

#[serde(
rename = "email",
Expand Down
Loading
Loading