Skip to content

Instantly share code, notes, and snippets.

@Grohden
Last active December 18, 2020 05:44
Show Gist options
  • Save Grohden/1fd8d110c806d8b239e88d07847c1e5a to your computer and use it in GitHub Desktop.
Save Grohden/1fd8d110c806d8b239e88d07847c1e5a to your computer and use it in GitHub Desktop.
{{>header}}
{{>part_of}}
class ApiClient {
ApiClient({this.basePath = '{{{basePath}}}'}) {
{{#hasAuthMethods}}
// Setup authentications (key: authentication name, value: authentication).
{{#authMethods}}
{{#isBasic}}
{{#isBasicBasic}}
_authentications[r'{{{name}}}'] = HttpBasicAuth();
{{/isBasicBasic}}
{{#isBasicBearer}}
_authentications[r'{{{name}}}'] = HttpBearerAuth();
{{/isBasicBearer}}
{{/isBasic}}
{{#isApiKey}}
_authentications[r'{{{name}}}'] = ApiKeyAuth({{#isKeyInCookie}}'cookie'{{/isKeyInCookie}}{{^isKeyInCookie}}{{#isKeyInHeader}}'header'{{/isKeyInHeader}}{{^isKeyInHeader}}'query'{{/isKeyInHeader}}{{/isKeyInCookie}}, '{{{keyParamName}}}');
{{/isApiKey}}
{{#isOAuth}}
_authentications[r'{{{name}}}'] = OAuth();
{{/isOAuth}}
{{/authMethods}}
{{/hasAuthMethods}}
}
final String basePath;
var _client = Client();
/// Returns the current HTTP [Client] instance to use in this class.
///
/// The return value is guaranteed to never be null.
Client get client => _client;
/// Requests to use a new HTTP [Client] in this class.
///
/// If the [newClient] is null, an [ArgumentError] is thrown.
set client(Client newClient) {
if (newClient == null) {
throw ArgumentError('New client instance cannot be null.');
}
_client = newClient;
}
final _defaultHeaderMap = <String, String>{};
final _authentications = <String, Authentication>{};
void addDefaultHeader(String key, String value) {
_defaultHeaderMap[key] = value;
}
dynamic deserialize(String json, String targetType, {bool growable}) {
// Remove all spaces. Necessary for reg expressions as well.
targetType = targetType.replaceAll(' ', '');
return targetType == 'String'
? json
: _deserialize(jsonDecode(json), targetType, growable: true == growable);
}
String serialize(Object obj) => obj == null ? '' : json.encode(obj);
T getAuthentication<T extends Authentication>(String name) {
final authentication = _authentications[name];
return authentication is T ? authentication : null;
}
// We don’t use a Map<String, String> for queryParams.
// If collectionFormat is 'multi', a key might appear multiple times.
Future<Response> invokeAPI(
String path,
String method,
List<QueryParam> queryParams,
Object body,
Map<String, String> headerParams,
Map<String, String> formParams,
String nullableContentType,
List<String> authNames,
) async {
_updateParamsForAuth(authNames, queryParams, headerParams);
headerParams.addAll(_defaultHeaderMap);
final urlEncodedQueryParams = queryParams
.where((param) => param.value != null)
.map((param) => '$param');
final queryString = urlEncodedQueryParams.isNotEmpty
? '?${urlEncodedQueryParams.join('&')}'
: '';
final url = '$basePath$path$queryString';
if (nullableContentType != null) {
headerParams['Content-Type'] = nullableContentType;
}
try {
// Special case for uploading a single file which isn’t a 'multipart/form-data'.
if (
body is MultipartFile && (nullableContentType == null ||
!nullableContentType.toLowerCase().startsWith('multipart/form-data'))
) {
final request = StreamedRequest(method, Uri.parse(url));
request.headers.addAll(headerParams);
request.contentLength = body.length;
body.finalize().listen(
request.sink.add,
onDone: request.sink.close,
onError: (error, trace) => request.sink.close(),
cancelOnError: true,
);
final response = await _client.send(request);
return Response.fromStream(response);
}
if (body is MultipartRequest) {
final request = MultipartRequest(method, Uri.parse(url));
request.fields.addAll(body.fields);
request.files.addAll(body.files);
request.headers.addAll(body.headers);
request.headers.addAll(headerParams);
final response = await _client.send(request);
return Response.fromStream(response);
}
final msgBody = nullableContentType == 'application/x-www-form-urlencoded'
? formParams
: serialize(body);
final nullableHeaderParams = headerParams.isEmpty ? null : headerParams;
switch(method) {
case 'POST': return await _client.post(url, headers: nullableHeaderParams, body: msgBody,);
case 'PUT': return await _client.put(url, headers: nullableHeaderParams, body: msgBody,);
case 'DELETE': return await _client.delete(url, headers: nullableHeaderParams,);
case 'PATCH': return await _client.patch(url, headers: nullableHeaderParams, body: msgBody,);
case 'HEAD': return await _client.head(url, headers: nullableHeaderParams,);
case 'GET': return await _client.get(url, headers: nullableHeaderParams,);
}
} on SocketException catch (e, trace) {
throw ApiException.withInner(HttpStatus.badRequest, 'Socket operation failed: $method $path', e, trace,);
} on TlsException catch (e, trace) {
throw ApiException.withInner(HttpStatus.badRequest, 'TLS/SSL communication failed: $method $path', e, trace,);
} on IOException catch (e, trace) {
throw ApiException.withInner(HttpStatus.badRequest, 'I/O operation failed: $method $path', e, trace,);
} on ClientException catch (e, trace) {
throw ApiException.withInner(HttpStatus.badRequest, 'HTTP connection failed: $method $path', e, trace,);
} on Exception catch (e, trace) {
throw ApiException.withInner(HttpStatus.badRequest, 'Exception occurred: $method $path', e, trace,);
}
throw ApiException(HttpStatus.badRequest, 'Invalid HTTP operation: $method $path',);
}
dynamic _deserialize(dynamic value, String targetType, {bool growable}) {
try {
switch (targetType) {
case 'String':
return '$value';
case 'int':
return value is int ? value : int.parse('$value');
case 'bool':
if (value is bool) {
return value;
}
final valueString = '$value'.toLowerCase();
return valueString == 'true' || valueString == '1';
break;
case 'double':
return value is double ? value : double.parse('$value');
{{#models}}
{{#model}}
case '{{{classname}}}':
{{#isEnum}}
return {{{classname}}}TypeTransformer().decode(value);
{{/isEnum}}
{{^isEnum}}
return {{{classname}}}.fromJson(value as Map<String, dynamic>);
{{/isEnum}}
{{/model}}
{{/models}}
default:
Match match;
if (value is List && (match = _regList.firstMatch(targetType)) != null) {
final newTargetType = match[1];
return value
.map((v) => _deserialize(v, newTargetType, growable: growable))
.toList(growable: true == growable);
}
if (value is Map && (match = _regMap.firstMatch(targetType)) != null) {
final newTargetType = match[1];
return Map.fromIterables(
value.keys,
value.values.map((v) => _deserialize(v, newTargetType, growable: growable)),
);
}
break;
}
} on Exception catch (e, stack) {
throw ApiException.withInner(HttpStatus.internalServerError, 'Exception during deserialization.', e, stack,);
}
throw ApiException(HttpStatus.internalServerError, 'Could not find a suitable class for deserialization',);
}
/// Update query and header parameters based on authentication settings.
/// @param authNames The authentications to apply
void _updateParamsForAuth(
List<String> authNames,
List<QueryParam> queryParams,
Map<String, String> headerParams,
) {
for(final authName in authNames) {
final auth = _authentications[authName];
if (auth == null) {
throw ArgumentError('Authentication undefined: $authName');
}
auth.applyToParams(queryParams, headerParams);
}
}
}
class {{{classname}}} {
/// Returns a new [{{{classname}}}] instance.
{{{classname}}}({
{{#vars}}
{{#required}}{{^defaultValue}}@required {{/defaultValue}}{{/required}}this.{{{name}}}{{^isNullable}}{{#defaultValue}} = {{#isEnum}}{{^isContainer}}const {{{classname}}}{{{enumName}}}._({{/isContainer}}{{/isEnum}}{{{defaultValue}}}{{#isEnum}}{{^isContainer}}){{/isContainer}}{{/isEnum}}{{/defaultValue}}{{/isNullable}},
{{/vars}}
});
{{#vars}}
{{#description}}
/// {{{description}}}
{{/description}}
{{^isEnum}}
{{#minimum}}
// minimum: {{{minimum}}}
{{/minimum}}
{{#maximum}}
// maximum: {{{maximum}}}
{{/maximum}}
{{/isEnum}}
final {{{datatypeWithEnum}}} {{{name}}};
{{/vars}}
@override
bool operator ==(Object other) => identical(this, other) || other is {{{classname}}} &&
{{#vars}}
other.{{{name}}} == {{{name}}}{{^-last}} &&{{/-last}}{{#-last}};{{/-last}}
{{/vars}}
@override
int get hashCode =>
{{#vars}}
({{{name}}} == null ? 0 : {{{name}}}.hashCode){{^-last}} +{{/-last}}{{#-last}};{{/-last}}
{{/vars}}
@override
String toString() => '{{{classname}}}[{{#vars}}{{{name}}}=${{{name}}}{{^-last}}, {{/-last}}{{/vars}}]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
{{#vars}}
if ({{{name}}} != null) {
{{#isDateTime}}
{{#pattern}}
json[r'{{{baseName}}}'] = _dateEpochMarker == '{{{pattern}}}'
? {{{name}}}.millisecondsSinceEpoch
: {{{name}}}.toUtc().toIso8601String();
{{/pattern}}
{{^pattern}}
json[r'{{{baseName}}}'] = {{{name}}}.toUtc().toIso8601String();
{{/pattern}}
{{/isDateTime}}
{{#isDate}}
{{#pattern}}
json[r'{{{baseName}}}'] = _dateEpochMarker == '{{{pattern}}}'
? {{{name}}}.millisecondsSinceEpoch
: _dateFormatter.format({{{name}}}.toUtc());
{{/pattern}}
{{^pattern}}
json[r'{{{baseName}}}'] = _dateFormatter.format({{{name}}}.toUtc());
{{/pattern}}
{{/isDate}}
{{^isDateTime}}
{{^isDate}}
json[r'{{{baseName}}}'] = {{{name}}};
{{/isDate}}
{{/isDateTime}}
}
{{/vars}}
return json;
}
/// Returns a new [{{{classname}}}] instance and imports its values from
/// [json] if it's non-null, null if [json] is null.
static {{{classname}}} fromJson(Map<String, dynamic> json) => json == null
? null
: {{{classname}}}(
{{#vars}}
{{#isDateTime}}
{{{name}}}: json[r'{{{baseName}}}'] == null
? null
{{#pattern}}
: _dateEpochMarker == '{{{pattern}}}'
? DateTime.fromMillisecondsSinceEpoch(json[r'{{{baseName}}}'] as int, isUtc: true)
: DateTime.parse(json[r'{{{baseName}}}'] as String),
{{/pattern}}
{{^pattern}}
: DateTime.parse(json[r'{{{baseName}}}'] as String),
{{/pattern}}
{{/isDateTime}}
{{#isDate}}
{{{name}}}: json[r'{{{baseName}}}'] == null
? null
{{#pattern}}
: _dateEpochMarker == '{{{pattern}}}'
? DateTime.fromMillisecondsSinceEpoch(json[r'{{{baseName}}}'] as int, isUtc: true)
: DateTime.parse(json[r'{{{baseName}}}'] as String),
{{/pattern}}
{{^pattern}}
: DateTime.parse(json[r'{{{baseName}}}'] as String),
{{/pattern}}
{{/isDate}}
{{^isDateTime}}
{{^isDate}}
{{#complexType}}
{{#isArray}}
{{#items.isArray}}
{{{name}}}: json[r'{{{baseName}}}'] == null
? null
: (json[r'{{{baseName}}}'] as List).map(
{{#items.complexType}}
{{items.complexType}}.listFromJson(json[r'{{{baseName}}}'] as List<dynamic>)
{{/items.complexType}}
{{^items.complexType}}
(e) => e == null ? null : (e as List).cast<{{items.items.dataType}}>()
{{/items.complexType}}
).toList(growable: false),
{{/items.isArray}}
{{^items.isArray}}
{{{name}}}: {{{complexType}}}.listFromJson(json[r'{{{baseName}}}'] as List<dynamic>),
{{/items.isArray}}
{{/isArray}}
{{^isArray}}
{{#isMap}}
{{#items.isArray}}
{{{name}}}: json[r'{{{baseName}}}'] == null
? null
{{#items.complexType}}
: {{items.complexType}}.mapListFromJson(json[r'{{{baseName}}}']),
{{/items.complexType}}
{{^items.complexType}}
: (json[r'{{{baseName}}}'] as Map).cast<String, List>(),
{{/items.complexType}}
{{/items.isArray}}
{{^items.isArray}}
{{{name}}}: json[r'{{{baseName}}}'] as Map<String, Map<String, String>>,
{{/items.isArray}}
{{/isMap}}
{{^isMap}}
{{#isBinary}}
{{{name}}}: null, // No support for decoding binary content from JSON
{{/isBinary}}
{{^isBinary}}
{{{name}}}: {{{complexType}}}.fromJson(json[r'{{{baseName}}}'] as Map<String, dynamic>),
{{/isBinary}}
{{/isMap}}
{{/isArray}}
{{/complexType}}
{{^complexType}}
{{#isArray}}
{{#isEnum}}
{{{name}}}: {{{classname}}}{{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']),
{{/isEnum}}
{{^isEnum}}
{{{name}}}: json[r'{{{baseName}}}'] == null
? null
: (json[r'{{{baseName}}}'] as List).cast<{{{items.datatype}}}>(),
{{/isEnum}}
{{/isArray}}
{{^isArray}}
{{#isMap}}
{{{name}}}: json[r'{{{baseName}}}'] == null ?
null :
(json[r'{{{baseName}}}'] as Map).cast<String, {{{items.datatype}}}>(),
{{/isMap}}
{{^isMap}}
{{#isNumber}}
{{{name}}}: json[r'{{{baseName}}}'] == null ?
null :
(json[r'{{{baseName}}}'] as num).toDouble(),
{{/isNumber}}
{{^isNumber}}
{{^isEnum}}
{{{name}}}: json[r'{{{baseName}}}'] as String,
{{/isEnum}}
{{#isEnum}}
{{{name}}}: {{{datatypeWithEnum}}}.fromJson(json[r'{{{baseName}}}'] as Map<String, dynamic>),
{{/isEnum}}
{{/isNumber}}
{{/isMap}}
{{/isArray}}
{{/complexType}}
{{/isDate}}
{{/isDateTime}}
{{/vars}}
);
static List<{{{classname}}}> listFromJson(List<dynamic> json, {bool emptyIsNull, bool growable,}) =>
json?.isEmpty == true
? true == emptyIsNull ? null : <{{{classname}}}>[]
: (json as List<Map<String, dynamic>>).map({{{classname}}}.fromJson).toList(growable: true == growable);
static Map<String, {{{classname}}}> mapFromJson(Map<String, dynamic> json) {
final map = <String, {{{classname}}}>{};
if (json?.isNotEmpty == true) {
json.forEach((key, value) => map[key] = {{{classname}}}.fromJson(value as Map<String, dynamic>));
}
return map;
}
// maps a json object with a list of {{{classname}}}-objects as value to a dart map
static Map<String, List<{{{classname}}}>> mapListFromJson(Map<String, dynamic> json, {bool emptyIsNull, bool growable,}) {
final map = <String, List<{{{classname}}}>>{};
if (json?.isNotEmpty == true) {
json.forEach((key, value) {
map[key] = {{{classname}}}.listFromJson(value as List<dynamic>, emptyIsNull: emptyIsNull, growable: growable,);
});
}
return map;
}
}
{{#vars}}
{{#isEnum}}
{{^isContainer}}
{{>enum_inline}}
{{/isContainer}}
{{#isContainer}}
{{#mostInnerItems}}
{{>enum_inline}}
{{/mostInnerItems}}
{{/isContainer}}
{{/isEnum}}
{{/vars}}
{{#description}}/// {{{description}}}{{/description}}
class {{{classname}}} {
/// Instantiate a new enum with the provided [value].
const {{{classname}}}._(this.value);
/// The underlying value of this enum member.
final {{{dataType}}} value;
@override
bool operator ==(Object other) => identical(this, other) ||
other is {{{classname}}} && other.value == value;
@override
int get hashCode => toString().hashCode;
@override
String toString() => value{{^isString}}.toString(){{/isString}};
{{{dataType}}} toJson() => value;
{{#allowableValues}}
{{#enumVars}}
static const {{{name}}} = {{{classname}}}._({{#isString}}r{{/isString}}{{{value}}});
{{/enumVars}}
{{/allowableValues}}
/// List of all possible values in this [enum][{{{classname}}}].
static const values = <{{{classname}}}>[
{{#allowableValues}}
{{#enumVars}}
{{{name}}},
{{/enumVars}}
{{/allowableValues}}
];
static {{{classname}}} fromJson(dynamic value) =>
{{{classname}}}TypeTransformer().decode(value);
static List<{{{classname}}}> listFromJson(List<dynamic> json, {bool emptyIsNull, bool growable,}) =>
json == null || json.isEmpty
? true == emptyIsNull ? null : <{{{classname}}}>[]
: json
.map({{{classname}}}.fromJson)
.toList(growable: true == growable);
}
/// Transformation class that can [encode] an instance of [{{{classname}}}] to {{{dataType}}},
/// and [decode] dynamic data back to [{{{classname}}}].
class {{{classname}}}TypeTransformer {
const {{{classname}}}TypeTransformer._();
factory {{{classname}}}TypeTransformer() => _instance ??= {{{classname}}}TypeTransformer._();
{{{dataType}}} encode({{{classname}}} data) => data.value;
/// Decodes a [dynamic value][data] to a {{{classname}}}.
///
/// 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.
{{{classname}}} decode(dynamic data, {bool allowNull}) {
switch (data as String) {
{{#allowableValues}}
{{#enumVars}}
case {{#isString}}r{{/isString}}{{{value}}}: return {{{classname}}}.{{{name}}};
{{/enumVars}}
{{/allowableValues}}
default:
if (allowNull == false) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
return null;
}
/// Singleton [{{{classname}}}TypeTransformer] instance.
static {{{classname}}}TypeTransformer _instance;
}
{{#description}}/// {{{description}}}{{/description}}
class {{{enumName}}} {
/// Instantiate a new enum with the provided [value].
const {{{enumName}}}._(this.value);
/// The underlying value of this enum member.
final {{{dataType}}} value;
@override
bool operator ==(Object other) => identical(this, other) ||
other is {{{enumName}}} && other.value == value;
@override
int get hashCode => toString().hashCode;
@override
String toString() => value{{^isString}}.toString(){{/isString}};
{{{dataType}}} toJson() => value;
{{#allowableValues}}
{{#enumVars}}
static const {{{name}}} = {{{enumName}}}._({{#isString}}r{{/isString}}{{{value}}});
{{/enumVars}}
{{/allowableValues}}
/// List of all possible values in this [enum][{{{enumName}}}].
static const values = <{{{enumName}}}>[
{{#allowableValues}}
{{#enumVars}}
{{{name}}},
{{/enumVars}}
{{/allowableValues}}
];
static {{{enumName}}} fromJson(dynamic value) =>
{{{enumName}}}TypeTransformer().decode(value);
static List<{{{enumName}}}> listFromJson(List<dynamic> json, {bool emptyIsNull, bool growable,}) =>
json == null || json.isEmpty
? true == emptyIsNull ? null : <{{{enumName}}}>[]
: json
.map((value) => {{{enumName}}}.fromJson(value))
.toList(growable: true == growable);
}
/// Transformation class that can [encode] an instance of [{{{enumName}}}] to {{{dataType}}},
/// and [decode] dynamic data back to [{{{enumName}}}].
class {{{enumName}}}TypeTransformer {
const {{{enumName}}}TypeTransformer._();
factory {{{enumName}}}TypeTransformer() => _instance ??= {{{enumName}}}TypeTransformer._();
{{{dataType}}} encode({{{enumName}}} data) => data.value;
/// Decodes a [dynamic value][data] to a {{{enumName}}}.
///
/// 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.
{{{enumName}}} decode(dynamic data, {bool allowNull}) {
switch (data as String) {
{{#allowableValues}}
{{#enumVars}}
case {{#isString}}r{{/isString}}{{{value}}}: return {{{enumName}}}.{{{name}}};
{{/enumVars}}
{{/allowableValues}}
default:
if (allowNull == false) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
return null;
}
/// Singleton [{{{enumName}}}TypeTransformer] instance.
static {{{enumName}}}TypeTransformer _instance;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment