aws-amplify / amplify-flutter

A declarative library with an easy-to-use interface for building Flutter applications on AWS.
https://docs.amplify.aws
Apache License 2.0
1.31k stars 245 forks source link

[smithy] Cannot compile with flutter master latest version #3986

Closed delfme closed 11 months ago

delfme commented 11 months ago

Description

Smithy package thrown below compile error:

../../.pub-cache/hosted/pub.dev/smithy-0.5.2/lib/src/http/http_operation.dart:271:16: Error: A value of type 'Object?' can't be assigned to a variable of type 'Output?'.
 - 'Object' is from 'dart:core'.
      output = switch (payload) {
               ^
Target kernel_snapshot failed: Exception

Can you please fix it? For internal reason we need to code on latest master.

Categories

Steps to Reproduce

To reproduce issue: 1) Upgrade to flutter latest master 2) Compile

Screenshots

No response

Platforms

Flutter Version

3.16.0-15.0.pre.27 Channel master

Amplify Flutter Version

1.4.1

Deployment Method

Amplify CLI

Schema

No response

delfme commented 11 months ago

I quick fixed the issue by changing these lines https://github.com/aws-amplify/amplify-flutter/blob/07fcfe18ce7f0da6db42833a5d8bc60571715288/packages/smithy/smithy/lib/src/http/http_operation.dart#L265-L266

Instead of

 Output? output;
 Object? error;

Use:

dynamic output;
dynamic error;

It needs test from you guys, but it seems to work.

Can you please add the fix to the package? I'd prefer not to use a fork for this.

delfme commented 11 months ago

Noticed I had to make difference changes

// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import 'dart:async';

import 'package:aws_common/aws_common.dart';
import 'package:built_value/serializer.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:smithy/ast.dart';
import 'package:smithy/smithy.dart';

@visibleForTesting
const zSmithyHttpTest = #_smithyHttpTest;

@internal
bool get isSmithyHttpTest => Zone.current[zSmithyHttpTest] as bool? ?? false;

/// Defines an operation which uses HTTP.
///
/// See: https://awslabs.github.io/smithy/1.0/spec/core/http-traits.html
abstract class HttpOperation<InputPayload, Input, OutputPayload, Output>
    with AWSDebuggable {
  /// Regex for label placeholders.
  static final _labelRegex = RegExp(r'{(\w+)}');

  /// Reserved characters defined in section 2.2 of RFC3986 and the % itself
  /// MUST be percent-encoded (that is, `:/?#[]@!$&'()*+,;=%`).
  ///
  /// Since [Uri.encodeQueryComponent] does not encode `+`, we must handle that
  /// separately as well.
  static String _escapeLabel(String label) =>
      Uri.encodeQueryComponent(label).replaceAll('+', '%20');

  /// Expands labels in [template] using [input].
  static String expandLabels(String template, HasLabel input) {
    final pattern = UriPattern.parse(template);
    return pattern.segments.map((segment) {
      return switch (segment.type) {
        SegmentType.literal => segment.content,
        SegmentType.label => _escapeLabel(input.labelFor(segment.content)),
        SegmentType.greedyLabel => input
            .labelFor(segment.content)
            .split('/')
            .map(_escapeLabel)
            .join('/'),
      };
    }).join('/');
  }

  static String expandHostLabel(String template, HasLabel input) {
    return template.replaceAllMapped(_labelRegex, (match) {
      final key = match.group(1)!;
      return _escapeLabel(input.labelFor(key));
    });
  }

  /// Builds the HTTP request for the given [input].
  HttpRequest buildRequest(Input input);

  /// Builds the output from the [payload] and metadata from the HTTP
  /// [response].
  Output buildOutput(
    OutputPayload payload,
    AWSBaseHttpResponse response,
  );

  /// The protocols used by this operation for all serialization/deserialization
  /// of wire formats.
  Iterable<HttpProtocol<InputPayload, Input, OutputPayload, Output>>
      get protocols;

  /// The error types of the operation.
  List<SmithyError> get errorTypes;

  /// The success code for the operation.
  ///
  /// Accepts the operation output since some output types embed the success
  /// code to allow for dynamic success codes.
  int successCode([Output? output]);

  /// The number of times the operation has been retried.
  @visibleForTesting
  int debugNumRetries = 0;

  /// The base URI for the operation.
  Uri get baseUri;

  /// The endpoint for the operation.
  Endpoint get endpoint => Endpoint(uri: baseUri);

  /// The retry handler for the operation.
  Retryer get retryer => const Retryer();

  @visibleForTesting
  HttpProtocol<InputPayload, Input, OutputPayload, Output> resolveProtocol({
    ShapeId? useProtocol,
  }) {
    return useProtocol == null
        ? protocols.first
        : protocols.firstWhere(
            (el) => el.protocolId == useProtocol,
            orElse: () => protocols.first,
          );
  }

  /// Generates the hostname for [request], given the [input] and whether the
  /// operation has a host prefix which needs expanding.
  String _hostForRequest(HttpRequest request, Input input, Uri baseUri) {
    final host = baseUri.host;
    var prefix = request.hostPrefix;
    if (!endpoint.isHostnameImmutable && prefix != null) {
      if (input is HasLabel) {
        prefix = expandHostLabel(prefix, input);
      }
      return '$prefix$host';
    }
    return host;
  }

  @visibleForTesting
  SmithyHttpRequest createRequest(
    HttpRequest request,
    HttpProtocol<InputPayload, Input, OutputPayload, Output> protocol,
    Input input,
  ) {
    final uri = baseUri;
    var path = request.path;

    // Expand `path` if it includes labels
    final pattern = UriPattern.parse(path);
    if (input is HasLabel) {
      path = expandLabels(path, input);
    } else if (pattern.labels.isNotEmpty) {
      throw MissingLabelException(input, pattern.labels.join(', '));
    }

    // Prevent duplicate `/` characters when joining with `basePath`.
    if (path.startsWith('/')) {
      path = path.substring(1);
    }

    // Calculate `path` relative to `baseUri`.
    final String basePath;
    if (uri.path.startsWith('/')) {
      basePath = uri.path.substring(1);
    } else {
      basePath = uri.path;
    }
    path = '$basePath/$path';

    // Correct for trailing slashes which may be necessary for signing, for ex,
    // but were removed due to [Uri] normalization.
    final needsTrailingSlash = request.path.split('?').first.endsWith('/');
    if (needsTrailingSlash && !path.endsWith('/')) {
      path += '/';
    }

    // Calculate remaining request parameters
    final host = _hostForRequest(request, input, uri);
    final headers = {
      ...protocol.headers,
      ...request.headers.asMap(),
    };
    final queryParameters = {
      for (final literal in pattern.queryLiterals.entries)
        literal.key: [literal.value],
      ...request.queryParameters.asMap(),
      ...uri.queryParametersAll,
    };
    final body = protocol.serialize(input);

    final awsRequest = AWSStreamedHttpRequest.raw(
      method: AWSHttpMethod.fromString(request.method),
      scheme: uri.scheme,
      host: host,
      port: uri.hasPort ? uri.port : null,
      path: path,
      body: body,
      queryParameters: queryParameters,
      headers: headers,
    );

    final requestInterceptors = List.of(protocol.requestInterceptors)
      ..addAll(request.requestInterceptors)
      ..sort((a, b) => a.order.compareTo(b.order));

    final responseInterceptors = protocol.responseInterceptors;

    return SmithyHttpRequest(
      awsRequest,
      requestInterceptors: requestInterceptors,
      responseInterceptors: responseInterceptors,
    );
  }

  @visibleForOverriding
  @visibleForTesting
  SmithyOperation<Output> send({
    required SmithyHttpRequest Function() createRequest,
    required HttpProtocol<InputPayload, Input, OutputPayload, Output> protocol,
    AWSHttpClient? client,
  }) {
    final requestProgress = StreamController<int>.broadcast(sync: true);
    final responseProgress = StreamController<int>.broadcast(sync: true);
    final operation = retryer.retry<Output>(
      () {
        // Recreate the request on each retry to perform signing again, etc.
        final httpRequest = createRequest();
        final operation = httpRequest.send(client: client);
        operation.requestProgress.forward(
          requestProgress,
          closeWhenDone: false,
        );
        operation.responseProgress.forward(
          responseProgress,
          closeWhenDone: false,
        );
        return operation.operation.then(
          (response) => deserializeOutput(
            protocol: protocol,
            response: response,
            // Prevents errors thrown from registering as "Uncaught Exceptions"
            // in the Dart debugger.
            //
            // This is a false positive because we do catch errors in the
            // retryer which wraps this. Likely this is due to the use of
            // completers in `CancelableOperation` or some other Zone-related
            // nonsense.
          ).catchError(Error.throwWithStackTrace),
        );
      },
      onCancel: () {
        requestProgress.close();
        responseProgress.close();
      },
      onRetry: (e, [delay]) {
        debugNumRetries++;
      },
    );
    return SmithyOperation(
      operation.then(
        (output) {
          requestProgress.close();
          responseProgress.close();
          return output;
        },
        onError: (e, st) {
          requestProgress.close();
          responseProgress.close();
          Error.throwWithStackTrace(e, st);
        },
      ),
      operationName: runtimeTypeName,
      requestProgress: requestProgress.stream,
      responseProgress: responseProgress.stream,
    );
  }

  @visibleForTesting
  Future<Output> deserializeOutput({
    required HttpProtocol<InputPayload, Input, OutputPayload, Output> protocol,
    required AWSBaseHttpResponse response,
  }) async {
    Output? output;
    dynamic error;
    StackTrace? stackTrace;
    var successCode = this.successCode();
    try {
      final payload = await protocol.deserialize(response.split());
      output = switch (payload) {
        Output _ => payload,
        _ => buildOutput(payload, response),
      };
      successCode = this.successCode(output);
    } on Object catch (e, st) {
      error = e;
      stackTrace = st;
    }
    if (response.statusCode == successCode) {
      // Close the response so that the underlying subscription created by
      // `split` is cancelled as well.
      unawaited(response.close());
      if (output != null) {
        return output;
      }
      Error.throwWithStackTrace(error!, stackTrace!);
    }

    try {
      SmithyError? smithyError;
      final resolvedType = await protocol.resolveErrorType(response);
      if (resolvedType != null) {
        smithyError =
            errorTypes.firstWhereOrNull((t) => t.shapeId.shape == resolvedType);
      }
      smithyError ??= errorTypes.singleWhereOrNull(
        (t) => t.statusCode == response.statusCode,
      );
      if (smithyError == null) {
        throw SmithyHttpException(
          statusCode: response.statusCode,
          body: await response.decodeBody(),
          headers: response.headers,
        );
      }
      final errorType = smithyError.type;
      final errorPayload = await protocol.wireSerializer.deserialize(
        await response.bodyBytes,
        specifiedType: FullType(errorType),
      );
      throw smithyError.build(errorPayload, response);
    } finally {
      // Close the response so that the underlying subscription created by
      // `split` is cancelled as well.
      unawaited(response.close());
    }
  }

  SmithyOperation<Output> run(
    Input input, {
    AWSHttpClient? client,
    ShapeId? useProtocol,
  }) {
    final protocol = resolveProtocol(useProtocol: useProtocol);
    client ??= protocol.getClient(input);
    final request = buildRequest(input);
    return send(
      createRequest: () => createRequest(
        request,
        protocol,
        input,
      ),
      client: client,
      protocol: protocol,
    );
  }
}

/// A version of [HttpOperation] which provides a convenient API for retrieving
/// pages of results.
abstract class PaginatedHttpOperation<
    InputPayload,
    Input,
    OutputPayload,
    Output,
    Token,
    PageSize,
    Items> extends HttpOperation<InputPayload, Input, OutputPayload, Output> {
  /// Retrieves the token from the operation output.
  Token? getToken(Output output);

  /// Retrieves the items from the operation output.
  Items getItems(Output output);

  /// Creates a new input with [token] and [pageSize].
  Input rebuildInput(Input input, Token token, PageSize? pageSize);

  /// Runs the operation returning a [PaginatedResult] which can be paged.
  SmithyOperation<PaginatedResult<Items, PageSize, Token>> runPaginated(
    Input input, {
    AWSHttpClient? client,
    ShapeId? useProtocol,
  }) {
    final operation = run(
      input,
      client: client,
      useProtocol: useProtocol,
    );
    final paginatedOperation = operation.operation.then((output) {
      final token = getToken(output);
      final items = getItems(output);
      late PaginatedResult<Items, PageSize, Token> result;
      return result = PaginatedResult(
        items,
        nextContinuationToken: token,

        // If there is a continuation token in the referenced outputToken member
        // of the response, then the client sends a subsequent request using the
        // same input parameters as the original call, but including the last
        // received continuation token. Clients are free to change the designated
        // pageSize input parameter at this step as needed.
        next: ([PageSize? pageSize]) {
          if (token == null) {
            return SmithyOperation(
              CancelableOperation.fromFuture(Future.value(result)),
              operationName: runtimeTypeName,
              requestProgress: const Stream.empty(),
              responseProgress: const Stream.empty(),
            );
          }
          return runPaginated(
            rebuildInput(input, token, pageSize),
            client: client,
            useProtocol: useProtocol,
          );
        },
      );
    });
    return SmithyOperation(
      paginatedOperation,
      operationName: runtimeTypeName,
      requestProgress: operation.requestProgress,
      responseProgress: operation.responseProgress,
    );
  }
}
Jordan-Nelson commented 11 months ago

Closing as a duplicate of https://github.com/aws-amplify/amplify-flutter/issues/3972