encoredev / encore

Development Platform for building robust type-safe distributed systems with declarative infrastructure
https://encore.dev
Mozilla Public License 2.0
6.49k stars 284 forks source link

Dart/Flutter client generation (DIO) #153

Open valpetli opened 2 years ago

valpetli commented 2 years ago

Our front-end is a Flutter application and it'd be awesome to add null-safe strongly typed Dart client generation to Encore. We can provide example client as well as open a PR to implement this feature.

We'd like to use Dio as a base client and we could start with implementing dart-dio generation and treat other implementations as separate languages as suggested by @eandre or try to come up with a more generic solution.

Additionally the resulting class can accept URL instead of environment name to handle custom URLs, PR and local environments.

How does that sound? 🙏

eandre commented 2 years ago

This sounds great! After some consideration I think a dio-specific generator is going to be easier to manage going forward than a pluggable one. What do you think?

valpetli commented 2 years ago

Sounds good @eandre, we'll create the example shortly then!

valpetli commented 2 years ago

@eandre We finally have a draft for the dart client:

import 'package:dio/dio.dart';

abstract class Client {
  Client(Dio dio) : serviceClient = ServiceClient(dio);

  final ServiceClient serviceClient;
}

class ServiceClient {
  ServiceClient(this.dio);

  final Dio dio;

  Future<void> DummyAPI(
      {required String boo, Foo? Foo, required dynamic Raw}) async {
    await dio.post<void>('/svc.DummyAPI', data: <String, dynamic>{
      'boo': boo,
      if (Foo != null) 'foo': Foo,
      'raw': Raw
    });
  }

  Future<void> Get({required String Baz}) async {
    await dio.get<void>('/svc.Get', queryParameters: <String, dynamic>{
      'boo': Baz,
    });
  }

  Future<void> RESTPath({required String a, required int b}) async {
    await dio.get<void>('/path/$a/$b');
  }

  Future<Tuple<bool, Foo>> TupleInputOutput(
      Tuple<String, WrappedRequest> params) async {
    final response =
        await dio.post<void>('/svc.TupleInputOutput', data: params);
    return Tuple<bool, Foo>.fromJson(response.data as Map<String, dynamic>);
  }
}

typedef Foo = num;

class Request {
  Request({this.foo, required this.boo, required this.raw});

  Foo? foo;
  String boo;
  dynamic raw;
}

class Wrapper<T> {}

class WrappedRequest<T> extends Wrapper<T> {}

const dynamic valueNotSet = 'valueNotSet';

class Tuple<A, B> {
  Tuple({required this.a, required this.b});

  final A a;
  final B b;

  Tuple<A, B> copyWith({
    A? a,
    B? b,
  }) =>
      _copyWith(a: a, b: b);

  Tuple<A, B> _copyWith({
    Object? a = valueNotSet,
    Object? b = valueNotSet,
  }) =>
      Tuple(
        a: (a == valueNotSet) ? this.a : (a as A),
        b: (b == valueNotSet) ? this.b : (b as B),
      );

  factory Tuple.fromJson(Map<String, dynamic> json) => Tuple(
        a: json['a'] as A,
        b: json['b'] as B,
      );

  Map<String, dynamic> toJson() => <String, dynamic>{'a': a, 'b': b};

   @override
  bool operator ==(Object other) =>
    identical(this, other) ||
    other is Tuple &&
    runtimeType == other.runtimeType &&
    a == other.a &&
    b == other.b;

  @override
  int get hashCode => toJson().hashCode;
}
valpetli commented 2 years ago

Oh and I forgot to mention, that we might need to split generated code in multiple files (one per service + umbrella client) as this seems to be the only way of implementing namespaces in Dart.

DomBlack commented 2 years ago

that we might need to split generated code in multiple files (one per service + umbrella client) as this seems to be the only way of implementing namespaces in Dart.

For the Go client, we've prefixed the structures with the service name, such that Request became SvcRequest and we had SvcFoo, would this work for Dart as well?

valpetli commented 2 years ago

@DomBlack Yeah that was a workaround we had in mind as well. The types would be have a stutter names like UsersUser or LikesLikes, but other than that it'll work ofc :-)

darthwade commented 1 year ago

+1

eandre commented 1 year ago

Hey @darthwade, thanks for the feedback. Could you share a bit more about your use case? Thanks!

darthwade commented 1 year ago

Hey @darthwade, thanks for the feedback. Could you share a bit more about your use case? Thanks!

Hey @eandre, first of all, great job with the encore! We're planning to build an app in Flutter and currently I'm researching tools for easy backend infrastructure. Encore looks great, the only 2 things I'd like to have are Dart client generation & gRPC, but second is not critical.

eandre commented 1 year ago

Thanks @darthwade, I'm really happy to hear that. If Flutter is critical for your use case we're happy to prioritize it. I'll send you an email to discuss further.