generalizeValueTypes function Null safety

ValueType generalizeValueTypes(
  1. List<ValueType> 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 [PandoraIdValueType]s, for example, should not be generalized as
  /// a [StringValueType], which would happen if a check against
  /// [StringValueType] 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 miscellaneous types --

  // Generalize Pandora ID/type types.
  if (allAre<PandoraIdValueType>()) {
    return PandoraIdValueType(optional: optional);
  } else if (allAre<PandoraTypeValueType>()) {
    return PandoraTypeValueType(optional: optional);
  }

  // Generalize timestamp values.
  if (allAre<TimestampValueType>()) {
    if (allAre<DateValueType>()) {
      return DateValueType(optional: optional);
    } else if (allAre<EpochTimestampValueType>()) {
      if (allAre<MicrosecondTimestampValueType>()) {
        return MicrosecondTimestampValueType(optional: optional);
      } else if (allAre<MillisecondTimestampValueType>()) {
        return MillisecondTimestampValueType(optional: optional);
      } else if (allAre<SecondTimestampValueType>()) {
        return SecondTimestampValueType(optional: optional);
      }
    }
  }

  // Generalize Iapetus entity types.
  if (allAre<IapetusEntityValueType>()) {
    if (allAre<AnnotationValueType>()) {
      return AnnotationValueType(optional: optional);
    }
  }

  // -- Generalize standard JSON types --

  // Generalize native types.
  if (allAre<NativeValueType>()) {
    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 NativeValueType(optional: optional);
  }

  // Generalize collection types.
  if (allAre<CollectionValueType>()) {
    // Generalize JSON object types.
    if (allAre<JsonObjectValueType>()) {
      // Generalize typed JSON map types.
      if (allAre<TypedJsonMapValueType>()) {
        if (allAre<AnnotationMapValueType>()) {
          return AnnotationMapValueType(optional: optional);
        }
        return TypedJsonMapValueType(
          keyValueType: generalizeValueTypes(
            relevantValueTypes
                .cast<TypedJsonMapValueType>()
                .map((valueType) => valueType.keyValueType)
                .toList(growable: false),
          ) as ValueType<String, dynamic>,
          valueValueType: 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(
        fieldValueTypes: 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,
        );
      }
    }
  }

  // TODO: Better support Pandora type object variants. Make a new value type
  // that maintains a map of pandora types to nested json object value types.
  // Support generalisation of that value type by merging the maps and
  // generalising collisions.
  //
  // Ideally, this should be able to help with things like annotations.

  // If no generalizations could be made, fall back on a [NativeValueType] 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 NativeValueType(optional: optional);
}