generalizeValueTypes function
Merges multiple ValueTypes together.
The resultant ValueType will correctly represent all of the given ValueTypes, while being as detailed as possible.
See also:
- ValueTypeInference, an extension on ValueType providing some shorthand functions for mutating ValueTypes.
Implementation
ValueType generalizeValueTypes(List<ValueType> valueTypes) {
// If any of the value types are optional, the final result must also be.
final bool optional = valueTypes.any((valueType) => valueType.optional);
// Ignore [UnknownValueType]s, as they're just placeholder values and should
// not be considered in generalization. Their only relevance is that their
// presence means that the generalized value type is optional, which has been
// accounted for.
final relevantValueTypes = valueTypes
.where((element) => element is! UnknownValueType)
.toList(growable: false);
// If, after removing [UnknownValueType]s, there are no value types remaining,
// then the value type remains unknown.
if (relevantValueTypes.isEmpty) {
return const UnknownValueType(optional: true);
}
// If there's only one value type to generalize, use it.
if (relevantValueTypes.length == 1) {
return relevantValueTypes.first.asOptional(optional: optional);
}
/// Generalize the given value types by matching them with common base types.
///
/// This must be done in a careful order, as specific types must not be
/// shadowed by prior matches against their base type.
///
/// A set of [IntegerValueType]s, for example, should not be generalized as
/// a [NumberValueType], which would happen if a check against
/// [NumberValueType] was done first.
///
/// To prevent these problems from occurring, the generalization process is
/// structured as follows:
///
/// - Subtype checks are generally grouped by if statements checking for their
/// base type. If their base type is concrete, it should be used if the
/// [valueTypes] do not all match just one.
/// - If subtype checks are not in their base type group, they must be
/// performed before the base type group check.
bool allAre<T>() => relevantValueTypes.every((valueType) => valueType is T);
// Generalize primitive types.
if (allAre<PrimitiveValueType>()) {
if (allAre<StringValueType>()) {
return StringValueType(optional: optional);
} else if (allAre<NumberValueType>()) {
if (allAre<IntegerValueType>()) {
return IntegerValueType(optional: optional);
} else if (allAre<DoubleValueType>()) {
return DoubleValueType(optional: optional);
}
return NumberValueType(optional: optional);
} else if (allAre<BooleanValueType>()) {
return BooleanValueType(optional: optional);
}
return PrimitiveValueType(optional: optional);
}
// Generalize collection types.
if (allAre<CollectionValueType>()) {
// Generalize JSON object types.
if (allAre<JsonObjectValueType>()) {
// Generalize typed JSON map types.
if (allAre<TypedJsonMapValueType>()) {
return TypedJsonMapValueType(
generalizeValueTypes(
relevantValueTypes
.cast<TypedJsonMapValueType>()
.map((valueType) => valueType.keyValueType)
.toList(growable: false),
) as ValueType<String, dynamic>,
generalizeValueTypes(
relevantValueTypes
.cast<TypedJsonMapValueType>()
.map((valueType) => valueType.valueValueType)
.toList(growable: false),
),
optional: optional,
);
}
// Generalize typed JSON object types.
// If other object types with unknown fields are present, ignore them.
final typedJsonObjectValueTypes = relevantValueTypes
.whereType<TypedJsonObjectValueType>()
.toList(growable: false);
return TypedJsonObjectValueType(
typedJsonObjectValueTypes
.cast<TypedJsonObjectValueType>()
.expand((valueType) => valueType.fieldValueTypes.entries)
.fold<Map<String, List<ValueType>>>(
{},
(valueTypeMap, valueDefinition) => valueTypeMap
..update(
valueDefinition.key,
(valueTypes) => valueTypes..add(valueDefinition.value),
ifAbsent: () => [valueDefinition.value],
),
).map((key, propertyValueTypes) {
var valueType = generalizeValueTypes(propertyValueTypes);
if (propertyValueTypes.length < relevantValueTypes.length) {
// If the amount of the property's value types is less than the
// amount of object value types, some objects must not have the
// property.
valueType = valueType.asOptional();
}
return MapEntry(key, valueType);
}),
optional: optional,
);
}
// Generalize list types.
if (allAre<ListValueType>()) {
// Generalize typed JSON list types.
if (allAre<TypedListValueType>()) {
return TypedListValueType(
generalizeValueTypes(
relevantValueTypes
.cast<TypedListValueType>()
.map((valueType) => valueType.elementValueType)
.toList(growable: false),
),
optional: optional,
);
}
}
}
// If no generalizations could be made, fall back on a [PrimitiveValueType] to
// treat the value as its native JSON/Dart type such as a string or map during
// parsing.
//
// Note that [UnknownValueType] is not appropriate here - the generalized
// type is not missing due to a lack of information, it's non-existent.
// If [UnknownValueType] were to be used here, the fact that a generalized
// type could not be determined would be lost during the next generalization
// pass, as the [UnknownValueType] will be ignored among any other value types
// in the list of value types to generalize.
return PrimitiveValueType<Object>(optional: optional);
}