Skip to content

Conversation

@0xNF
Copy link
Contributor

@0xNF 0xNF commented Feb 7, 2024

fix #17547
fix #13459

This PR is part of the effort to split PR #17548

Numeric enums of the form:

some_enum:
  type: integer
  enum:
    - 1
    - 2

will now generate correct code, instead of invalid, non compile-able dart code.

Enums with a default choice will now also generate valid dart code for instantiating that default:

some_enum2:
  type: string
  enum:
    - "x"
    - "y"
  default: "x"

Additionally, I added hashCode and == override to these enums which check against their inner values. Previously, equality checks would check against the object in memory, because these are kind of fake pseudo enums instead of actual dart supported keyword enums.

@jaumard (2018/09) @josh-burton (2019/12) @amondnet (2019/12) @sbu-WBT (2020/12) @kuhnroyal (2020/12) @agilob (2020/12) @ahmednfwela (2021/08)

…s#17547) & (OpenAPITools#13459).

* Generated Enums now have equality and hashcode methods checking against their in-memory id then against their underlying value
* Classes with a generated Numeric Enum in their constructor enums now instantiate sytactically correctly, without causing compile errors
* Classes with default-available generated Enum in their fromJson() method now use a syntactically correct default, without causing compile errors
@kuhnroyal
Copy link
Contributor

Need to regenerate samples, seems you fixed the extra whitespace which I was going to ask about :)
There is no occurrence of such an enum in the samples?

@0xNF
Copy link
Contributor Author

0xNF commented Feb 16, 2024

Here's the spec file I'm working with:

spec file
openapi: 3.0.3
info:
  version: "1.1"
  title: Dart Uint8list Demo
servers:
  - url: "localhost"
    variables:
      host:
        default: localhost
paths:
  /item:
    get:
      operationId: GetItem
      description: "Should return an Item"
      responses:
        "200":
          description: items
          content:
            application/json:
              schema:
                $ref: "#components/schemas/item"
components:
  schemas:
    WithEnums:
      type: object
      properties:
        stringEnum:
          type: string
          enum:
            - strA
            - strB
            - strC
          default: strC
        intEnum:
          type: integer
          enum:
            - 0
            - 1
            - 2
          default: 2
        boolEnum:
          type: boolean
          enum:
            - true
            - false
          default: true

Some sample generated code:

generated dart enum
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12

// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars

part of lol;

class WithEnums {
  /// Returns a new [WithEnums] instance.
  WithEnums({
    this.stringEnum = const WithEnumsStringEnumEnum._('strC'),
    this.intEnum = WithEnumsIntEnumEnum.number2,
    this.boolEnum = true,
  });

  WithEnumsStringEnumEnum stringEnum;

  WithEnumsIntEnumEnum intEnum;

  bool boolEnum;

  @override
  bool operator ==(Object other) => identical(this, other) || other is WithEnums && other.stringEnum == stringEnum && other.intEnum == intEnum && other.boolEnum == boolEnum;

  @override
  int get hashCode =>
      // ignore: unnecessary_parenthesis
      (stringEnum.hashCode) + (intEnum.hashCode) + (boolEnum.hashCode);

  @override
  String toString() => 'WithEnums[stringEnum=$stringEnum, intEnum=$intEnum, boolEnum=$boolEnum]';

  Map<String, dynamic> toJson() {
    final json = <String, dynamic>{};
    json[r'stringEnum'] = this.stringEnum;
    json[r'intEnum'] = this.intEnum;
    json[r'boolEnum'] = this.boolEnum;
    return json;
  }

  /// Returns a new [WithEnums] instance and imports its values from
  /// [value] if it's a [Map], null otherwise.
  // ignore: prefer_constructors_over_static_methods
  static WithEnums? fromJson(dynamic value) {
    if (value is Map) {
      final json = value.cast<String, dynamic>();

      // Ensure that the map contains the required keys.
      // Note 1: the values aren't checked for validity beyond being non-null.
      // Note 2: this code is stripped in release mode!
      assert(() {
        requiredKeys.forEach((key) {
          assert(json.containsKey(key), 'Required key "WithEnums[$key]" is missing from JSON.');
          assert(json[key] != null, 'Required key "WithEnums[$key]" has a null value in JSON.');
        });
        return true;
      }());

      return WithEnums(
        stringEnum: WithEnumsStringEnumEnum.fromJson(json[r'stringEnum']) ?? const WithEnumsStringEnumEnum._('strC'),
        intEnum: WithEnumsIntEnumEnum.fromJson(json[r'intEnum']) ?? WithEnumsIntEnumEnum.number2,
        boolEnum: mapValueOfType<bool>(json, r'boolEnum') ?? true,
      );
    }
    return null;
  }

  static List<WithEnums> listFromJson(
    dynamic json, {
    bool growable = false,
  }) {
    final result = <WithEnums>[];
    if (json is List && json.isNotEmpty) {
      for (final row in json) {
        final value = WithEnums.fromJson(row);
        if (value != null) {
          result.add(value);
        }
      }
    }
    return result.toList(growable: growable);
  }

  static Map<String, WithEnums> mapFromJson(dynamic json) {
    final map = <String, WithEnums>{};
    if (json is Map && json.isNotEmpty) {
      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
      for (final entry in json.entries) {
        final value = WithEnums.fromJson(entry.value);
        if (value != null) {
          map[entry.key] = value;
        }
      }
    }
    return map;
  }

  // maps a json object with a list of WithEnums-objects as value to a dart map
  static Map<String, List<WithEnums>> mapListFromJson(
    dynamic json, {
    bool growable = false,
  }) {
    final map = <String, List<WithEnums>>{};
    if (json is Map && json.isNotEmpty) {
      // ignore: parameter_assignments
      json = json.cast<String, dynamic>();
      for (final entry in json.entries) {
        map[entry.key] = WithEnums.listFromJson(
          entry.value,
          growable: growable,
        );
      }
    }
    return map;
  }

  /// The list of required keys that must be present in a JSON.
  static const requiredKeys = <String>{};
}

class WithEnumsStringEnumEnum {
  /// Instantiate a new enum with the provided [value].
  const WithEnumsStringEnumEnum._(this.value);

  /// The underlying value of this enum member.
  final String value;

  @override
  String toString() => value;

  String toJson() => value;

  static const strA = WithEnumsStringEnumEnum._(r'strA');
  static const strB = WithEnumsStringEnumEnum._(r'strB');
  static const strC = WithEnumsStringEnumEnum._(r'strC');

  /// List of all possible values in this [enum][WithEnumsStringEnumEnum].
  static const values = <WithEnumsStringEnumEnum>[
    strA,
    strB,
    strC,
  ];

  static WithEnumsStringEnumEnum? fromJson(dynamic value) => WithEnumsStringEnumEnumTypeTransformer().decode(value);

  static List<WithEnumsStringEnumEnum> listFromJson(
    dynamic json, {
    bool growable = false,
  }) {
    final result = <WithEnumsStringEnumEnum>[];
    if (json is List && json.isNotEmpty) {
      for (final row in json) {
        final value = WithEnumsStringEnumEnum.fromJson(row);
        if (value != null) {
          result.add(value);
        }
      }
    }
    return result.toList(growable: growable);
  }

  @override
  bool operator ==(Object other) => identical(this, other) || other is WithEnumsStringEnumEnum && other.value == value;

  @override
  int get hashCode => value.hashCode;
}

/// Transformation class that can [encode] an instance of [WithEnumsStringEnumEnum] to String,
/// and [decode] dynamic data back to [WithEnumsStringEnumEnum].
class WithEnumsStringEnumEnumTypeTransformer {
  factory WithEnumsStringEnumEnumTypeTransformer() => _instance ??= const WithEnumsStringEnumEnumTypeTransformer._();

  const WithEnumsStringEnumEnumTypeTransformer._();

  String encode(WithEnumsStringEnumEnum data) => data.value;

  /// Decodes a [dynamic value][data] to a WithEnumsStringEnumEnum.
  ///
  /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
  /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
  /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
  ///
  /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
  /// and users are still using an old app with the old code.
  WithEnumsStringEnumEnum? decode(dynamic data, {bool allowNull = true}) {
    if (data != null) {
      switch (data) {
        case r'strA':
          return WithEnumsStringEnumEnum.strA;
        case r'strB':
          return WithEnumsStringEnumEnum.strB;
        case r'strC':
          return WithEnumsStringEnumEnum.strC;
        default:
          if (!allowNull) {
            throw ArgumentError('Unknown enum value to decode: $data');
          }
      }
    }
    return null;
  }

  /// Singleton [WithEnumsStringEnumEnumTypeTransformer] instance.
  static WithEnumsStringEnumEnumTypeTransformer? _instance;
}

class WithEnumsIntEnumEnum {
  /// Instantiate a new enum with the provided [value].
  const WithEnumsIntEnumEnum._(this.value);

  /// The underlying value of this enum member.
  final int value;

  @override
  String toString() => value.toString();

  int toJson() => value;

  static const number0 = WithEnumsIntEnumEnum._(0);
  static const number1 = WithEnumsIntEnumEnum._(1);
  static const number2 = WithEnumsIntEnumEnum._(2);

  /// List of all possible values in this [enum][WithEnumsIntEnumEnum].
  static const values = <WithEnumsIntEnumEnum>[
    number0,
    number1,
    number2,
  ];

  static WithEnumsIntEnumEnum? fromJson(dynamic value) => WithEnumsIntEnumEnumTypeTransformer().decode(value);

  static List<WithEnumsIntEnumEnum> listFromJson(
    dynamic json, {
    bool growable = false,
  }) {
    final result = <WithEnumsIntEnumEnum>[];
    if (json is List && json.isNotEmpty) {
      for (final row in json) {
        final value = WithEnumsIntEnumEnum.fromJson(row);
        if (value != null) {
          result.add(value);
        }
      }
    }
    return result.toList(growable: growable);
  }

  @override
  bool operator ==(Object other) => identical(this, other) || other is WithEnumsIntEnumEnum && other.value == value;

  @override
  int get hashCode => value.hashCode;
}

/// Transformation class that can [encode] an instance of [WithEnumsIntEnumEnum] to int,
/// and [decode] dynamic data back to [WithEnumsIntEnumEnum].
class WithEnumsIntEnumEnumTypeTransformer {
  factory WithEnumsIntEnumEnumTypeTransformer() => _instance ??= const WithEnumsIntEnumEnumTypeTransformer._();

  const WithEnumsIntEnumEnumTypeTransformer._();

  int encode(WithEnumsIntEnumEnum data) => data.value;

  /// Decodes a [dynamic value][data] to a WithEnumsIntEnumEnum.
  ///
  /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
  /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
  /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
  ///
  /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
  /// and users are still using an old app with the old code.
  WithEnumsIntEnumEnum? decode(dynamic data, {bool allowNull = true}) {
    if (data != null) {
      switch (data) {
        case 0:
          return WithEnumsIntEnumEnum.number0;
        case 1:
          return WithEnumsIntEnumEnum.number1;
        case 2:
          return WithEnumsIntEnumEnum.number2;
        default:
          if (!allowNull) {
            throw ArgumentError('Unknown enum value to decode: $data');
          }
      }
    }
    return null;
  }

  /// Singleton [WithEnumsIntEnumEnumTypeTransformer] instance.
  static WithEnumsIntEnumEnumTypeTransformer? _instance;
}

The key bit is this portion:

  WithEnums({
    this.stringEnum = const WithEnumsStringEnumEnum._('strC'),
    this.intEnum = WithEnumsIntEnumEnum.number2,
    this.boolEnum = true,
  });

This is now valid dart code. It's a bit syntactically wonky because the generator is producing the following mustache variable output for the defaultValue field:

      "defaultValue" : "WithEnumsIntEnumEnum.number2",

vs

      "defaultValue" : "'strC'",

But otherwise, the code is technically correct.

@kuhnroyal
Copy link
Contributor

Looks fine for me but someone using the dart generator should review this as well.

…r numeric enums with default values in FromJson methods
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants