diff --git a/packages/better_networking/.gitignore b/packages/better_networking/.gitignore new file mode 100644 index 00000000..eb6c05cd --- /dev/null +++ b/packages/better_networking/.gitignore @@ -0,0 +1,31 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +build/ diff --git a/packages/better_networking/CHANGELOG.md b/packages/better_networking/CHANGELOG.md new file mode 100644 index 00000000..41cc7d81 --- /dev/null +++ b/packages/better_networking/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/better_networking/LICENSE b/packages/better_networking/LICENSE new file mode 100644 index 00000000..ba75c69f --- /dev/null +++ b/packages/better_networking/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/better_networking/README.md b/packages/better_networking/README.md new file mode 100644 index 00000000..400c9425 --- /dev/null +++ b/packages/better_networking/README.md @@ -0,0 +1 @@ +## better_networking \ No newline at end of file diff --git a/packages/better_networking/analysis_options.yaml b/packages/better_networking/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/packages/better_networking/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/better_networking/lib/better_networking.dart b/packages/better_networking/lib/better_networking.dart new file mode 100644 index 00000000..2462c456 --- /dev/null +++ b/packages/better_networking/lib/better_networking.dart @@ -0,0 +1,14 @@ +library apidash_core; + +export 'consts.dart'; +export 'extensions/extensions.dart'; +export 'models/models.dart'; +export 'utils/utils.dart'; +export 'services/services.dart'; + +// Export 3rd party packages +export 'package:collection/collection.dart'; +export 'package:freezed_annotation/freezed_annotation.dart'; +export 'package:http/http.dart'; +export 'package:http_parser/http_parser.dart'; +export 'package:seed/seed.dart'; diff --git a/packages/better_networking/lib/consts.dart b/packages/better_networking/lib/consts.dart new file mode 100644 index 00000000..ac52f9e0 --- /dev/null +++ b/packages/better_networking/lib/consts.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; + +enum APIType { + rest("HTTP", "HTTP"), + graphql("GraphQL", "GQL"); + + const APIType(this.label, this.abbr); + final String label; + final String abbr; +} + +enum HTTPVerb { + get("GET"), + head("HEAD"), + post("POST"), + put("PUT"), + patch("PAT"), + delete("DEL"), + options("OPT"); + + const HTTPVerb(this.abbr); + final String abbr; +} + +enum SupportedUriSchemes { https, http } + +final kSupportedUriSchemes = SupportedUriSchemes.values + .map((i) => i.name) + .toList(); +const kDefaultUriScheme = SupportedUriSchemes.https; +final kLocalhostRegex = RegExp(r'^localhost(:\d+)?(/.*)?$'); +final kIPHostRegex = RegExp( + r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(:\d+)?(/.*)?$', +); + +const kMethodsWithBody = [ + HTTPVerb.post, + HTTPVerb.put, + HTTPVerb.patch, + HTTPVerb.delete, +]; + +const kDefaultHttpMethod = HTTPVerb.get; +const kDefaultContentType = ContentType.json; + +const kTypeApplication = 'application'; +// application +const kSubTypeJson = 'json'; +const kSubTypeOctetStream = 'octet-stream'; +const kSubTypePdf = 'pdf'; +const kSubTypeSql = 'sql'; +const kSubTypeXml = 'xml'; +const kSubTypeYaml = 'yaml'; +const kSubTypeXYaml = 'x-yaml'; +const kSubTypeYml = 'x-yml'; +const kSubTypeXWwwFormUrlencoded = 'x-www-form-urlencoded'; + +const kTypeText = 'text'; +// text +const kSubTypeCss = 'css'; +const kSubTypeCsv = 'csv'; +const kSubTypeHtml = 'html'; +const kSubTypeJavascript = 'javascript'; +const kSubTypeMarkdown = 'markdown'; +const kSubTypePlain = 'plain'; +const kSubTypeTextXml = 'xml'; +const kSubTypeTextYaml = 'yaml'; +const kSubTypeTextYml = 'yml'; + +const kTypeImage = 'image'; +//image +const kSubTypeSvg = 'svg+xml'; + +const kTypeAudio = 'audio'; +const kTypeVideo = 'video'; + +const kTypeMultipart = "multipart"; +const kSubTypeFormData = "form-data"; + +const kSubTypeDefaultViewOptions = 'all'; + +enum ContentType { + json("$kTypeApplication/$kSubTypeJson"), + text("$kTypeText/$kSubTypePlain"), + formdata("$kTypeMultipart/$kSubTypeFormData"); + + const ContentType(this.header); + final String header; +} + +const JsonEncoder kJsonEncoder = JsonEncoder.withIndent(' '); +const JsonDecoder kJsonDecoder = JsonDecoder(); +const LineSplitter kSplitter = LineSplitter(); + +const kCodeCharsPerLineLimit = 200; + +const kHeaderContentType = "Content-Type"; +const kMsgRequestCancelled = 'Request Cancelled'; diff --git a/packages/better_networking/lib/extensions/extensions.dart b/packages/better_networking/lib/extensions/extensions.dart new file mode 100644 index 00000000..0c21fc86 --- /dev/null +++ b/packages/better_networking/lib/extensions/extensions.dart @@ -0,0 +1 @@ +export 'map_extensions.dart'; diff --git a/packages/better_networking/lib/extensions/map_extensions.dart b/packages/better_networking/lib/extensions/map_extensions.dart new file mode 100644 index 00000000..bfafbb15 --- /dev/null +++ b/packages/better_networking/lib/extensions/map_extensions.dart @@ -0,0 +1,37 @@ +import 'dart:io'; + +extension MapExtension on Map { + bool hasKeyContentType() { + return keys.any( + (k) => (k is String) + ? k.toLowerCase() == HttpHeaders.contentTypeHeader + : false, + ); + } + + String? getKeyContentType() { + if (isEmpty) { + return null; + } + bool present = hasKeyContentType(); + if (present) { + return keys.firstWhere( + (e) => (e is String) + ? e.toLowerCase() == HttpHeaders.contentTypeHeader + : false, + ); + } + return null; + } + + String? getValueContentType() { + return this[getKeyContentType()]; + } + + Map removeKeyContentType() { + removeWhere( + (key, value) => key.toLowerCase() == HttpHeaders.contentTypeHeader, + ); + return this; + } +} diff --git a/packages/better_networking/lib/models/http_request_model.dart b/packages/better_networking/lib/models/http_request_model.dart new file mode 100644 index 00000000..cc2d7411 --- /dev/null +++ b/packages/better_networking/lib/models/http_request_model.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:seed/seed.dart'; +import '../extensions/extensions.dart'; +import '../utils/utils.dart' + show rowsToFormDataMapList, rowsToMap, getEnabledRows; +import '../consts.dart'; + +part 'http_request_model.freezed.dart'; +part 'http_request_model.g.dart'; + +@freezed +class HttpRequestModel with _$HttpRequestModel { + const HttpRequestModel._(); + + @JsonSerializable( + explicitToJson: true, + anyMap: true, + ) + const factory HttpRequestModel({ + @Default(HTTPVerb.get) HTTPVerb method, + @Default("") String url, + List? headers, + List? params, + List? isHeaderEnabledList, + List? isParamEnabledList, + @Default(ContentType.json) ContentType bodyContentType, + String? body, + String? query, + List? formData, + }) = _HttpRequestModel; + + factory HttpRequestModel.fromJson(Map json) => + _$HttpRequestModelFromJson(json); + + Map get headersMap => rowsToMap(headers) ?? {}; + Map get paramsMap => rowsToMap(params) ?? {}; + List? get enabledHeaders => + getEnabledRows(headers, isHeaderEnabledList); + List? get enabledParams => + getEnabledRows(params, isParamEnabledList); + + Map get enabledHeadersMap => rowsToMap(enabledHeaders) ?? {}; + Map get enabledParamsMap => rowsToMap(enabledParams) ?? {}; + + bool get hasContentTypeHeader => enabledHeadersMap.hasKeyContentType(); + bool get hasFormDataContentType => bodyContentType == ContentType.formdata; + bool get hasJsonContentType => bodyContentType == ContentType.json; + bool get hasTextContentType => bodyContentType == ContentType.text; + int get contentLength => utf8.encode(body ?? "").length; + bool get hasBody => hasJsonData || hasTextData || hasFormData; + bool get hasJsonData => + kMethodsWithBody.contains(method) && + hasJsonContentType && + contentLength > 0; + bool get hasTextData => + kMethodsWithBody.contains(method) && + hasTextContentType && + contentLength > 0; + bool get hasFormData => + kMethodsWithBody.contains(method) && + hasFormDataContentType && + formDataMapList.isNotEmpty; + bool get hasQuery => query?.isNotEmpty ?? false; + List get formDataList => formData ?? []; + List> get formDataMapList => + rowsToFormDataMapList(formDataList) ?? []; + bool get hasFileInFormData => formDataList + .map((e) => e.type == FormDataType.file) + .any((element) => element); +} diff --git a/packages/better_networking/lib/models/http_request_model.freezed.dart b/packages/better_networking/lib/models/http_request_model.freezed.dart new file mode 100644 index 00000000..38cd0132 --- /dev/null +++ b/packages/better_networking/lib/models/http_request_model.freezed.dart @@ -0,0 +1,415 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'http_request_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +HttpRequestModel _$HttpRequestModelFromJson(Map json) { + return _HttpRequestModel.fromJson(json); +} + +/// @nodoc +mixin _$HttpRequestModel { + HTTPVerb get method => throw _privateConstructorUsedError; + String get url => throw _privateConstructorUsedError; + List? get headers => throw _privateConstructorUsedError; + List? get params => throw _privateConstructorUsedError; + List? get isHeaderEnabledList => throw _privateConstructorUsedError; + List? get isParamEnabledList => throw _privateConstructorUsedError; + ContentType get bodyContentType => throw _privateConstructorUsedError; + String? get body => throw _privateConstructorUsedError; + String? get query => throw _privateConstructorUsedError; + List? get formData => throw _privateConstructorUsedError; + + /// Serializes this HttpRequestModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of HttpRequestModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $HttpRequestModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HttpRequestModelCopyWith<$Res> { + factory $HttpRequestModelCopyWith( + HttpRequestModel value, $Res Function(HttpRequestModel) then) = + _$HttpRequestModelCopyWithImpl<$Res, HttpRequestModel>; + @useResult + $Res call( + {HTTPVerb method, + String url, + List? headers, + List? params, + List? isHeaderEnabledList, + List? isParamEnabledList, + ContentType bodyContentType, + String? body, + String? query, + List? formData}); +} + +/// @nodoc +class _$HttpRequestModelCopyWithImpl<$Res, $Val extends HttpRequestModel> + implements $HttpRequestModelCopyWith<$Res> { + _$HttpRequestModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of HttpRequestModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? method = null, + Object? url = null, + Object? headers = freezed, + Object? params = freezed, + Object? isHeaderEnabledList = freezed, + Object? isParamEnabledList = freezed, + Object? bodyContentType = null, + Object? body = freezed, + Object? query = freezed, + Object? formData = freezed, + }) { + return _then(_value.copyWith( + method: null == method + ? _value.method + : method // ignore: cast_nullable_to_non_nullable + as HTTPVerb, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + headers: freezed == headers + ? _value.headers + : headers // ignore: cast_nullable_to_non_nullable + as List?, + params: freezed == params + ? _value.params + : params // ignore: cast_nullable_to_non_nullable + as List?, + isHeaderEnabledList: freezed == isHeaderEnabledList + ? _value.isHeaderEnabledList + : isHeaderEnabledList // ignore: cast_nullable_to_non_nullable + as List?, + isParamEnabledList: freezed == isParamEnabledList + ? _value.isParamEnabledList + : isParamEnabledList // ignore: cast_nullable_to_non_nullable + as List?, + bodyContentType: null == bodyContentType + ? _value.bodyContentType + : bodyContentType // ignore: cast_nullable_to_non_nullable + as ContentType, + body: freezed == body + ? _value.body + : body // ignore: cast_nullable_to_non_nullable + as String?, + query: freezed == query + ? _value.query + : query // ignore: cast_nullable_to_non_nullable + as String?, + formData: freezed == formData + ? _value.formData + : formData // ignore: cast_nullable_to_non_nullable + as List?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$HttpRequestModelImplCopyWith<$Res> + implements $HttpRequestModelCopyWith<$Res> { + factory _$$HttpRequestModelImplCopyWith(_$HttpRequestModelImpl value, + $Res Function(_$HttpRequestModelImpl) then) = + __$$HttpRequestModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {HTTPVerb method, + String url, + List? headers, + List? params, + List? isHeaderEnabledList, + List? isParamEnabledList, + ContentType bodyContentType, + String? body, + String? query, + List? formData}); +} + +/// @nodoc +class __$$HttpRequestModelImplCopyWithImpl<$Res> + extends _$HttpRequestModelCopyWithImpl<$Res, _$HttpRequestModelImpl> + implements _$$HttpRequestModelImplCopyWith<$Res> { + __$$HttpRequestModelImplCopyWithImpl(_$HttpRequestModelImpl _value, + $Res Function(_$HttpRequestModelImpl) _then) + : super(_value, _then); + + /// Create a copy of HttpRequestModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? method = null, + Object? url = null, + Object? headers = freezed, + Object? params = freezed, + Object? isHeaderEnabledList = freezed, + Object? isParamEnabledList = freezed, + Object? bodyContentType = null, + Object? body = freezed, + Object? query = freezed, + Object? formData = freezed, + }) { + return _then(_$HttpRequestModelImpl( + method: null == method + ? _value.method + : method // ignore: cast_nullable_to_non_nullable + as HTTPVerb, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + headers: freezed == headers + ? _value._headers + : headers // ignore: cast_nullable_to_non_nullable + as List?, + params: freezed == params + ? _value._params + : params // ignore: cast_nullable_to_non_nullable + as List?, + isHeaderEnabledList: freezed == isHeaderEnabledList + ? _value._isHeaderEnabledList + : isHeaderEnabledList // ignore: cast_nullable_to_non_nullable + as List?, + isParamEnabledList: freezed == isParamEnabledList + ? _value._isParamEnabledList + : isParamEnabledList // ignore: cast_nullable_to_non_nullable + as List?, + bodyContentType: null == bodyContentType + ? _value.bodyContentType + : bodyContentType // ignore: cast_nullable_to_non_nullable + as ContentType, + body: freezed == body + ? _value.body + : body // ignore: cast_nullable_to_non_nullable + as String?, + query: freezed == query + ? _value.query + : query // ignore: cast_nullable_to_non_nullable + as String?, + formData: freezed == formData + ? _value._formData + : formData // ignore: cast_nullable_to_non_nullable + as List?, + )); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, anyMap: true) +class _$HttpRequestModelImpl extends _HttpRequestModel { + const _$HttpRequestModelImpl( + {this.method = HTTPVerb.get, + this.url = "", + final List? headers, + final List? params, + final List? isHeaderEnabledList, + final List? isParamEnabledList, + this.bodyContentType = ContentType.json, + this.body, + this.query, + final List? formData}) + : _headers = headers, + _params = params, + _isHeaderEnabledList = isHeaderEnabledList, + _isParamEnabledList = isParamEnabledList, + _formData = formData, + super._(); + + factory _$HttpRequestModelImpl.fromJson(Map json) => + _$$HttpRequestModelImplFromJson(json); + + @override + @JsonKey() + final HTTPVerb method; + @override + @JsonKey() + final String url; + final List? _headers; + @override + List? get headers { + final value = _headers; + if (value == null) return null; + if (_headers is EqualUnmodifiableListView) return _headers; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + final List? _params; + @override + List? get params { + final value = _params; + if (value == null) return null; + if (_params is EqualUnmodifiableListView) return _params; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + final List? _isHeaderEnabledList; + @override + List? get isHeaderEnabledList { + final value = _isHeaderEnabledList; + if (value == null) return null; + if (_isHeaderEnabledList is EqualUnmodifiableListView) + return _isHeaderEnabledList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + final List? _isParamEnabledList; + @override + List? get isParamEnabledList { + final value = _isParamEnabledList; + if (value == null) return null; + if (_isParamEnabledList is EqualUnmodifiableListView) + return _isParamEnabledList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + @JsonKey() + final ContentType bodyContentType; + @override + final String? body; + @override + final String? query; + final List? _formData; + @override + List? get formData { + final value = _formData; + if (value == null) return null; + if (_formData is EqualUnmodifiableListView) return _formData; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'HttpRequestModel(method: $method, url: $url, headers: $headers, params: $params, isHeaderEnabledList: $isHeaderEnabledList, isParamEnabledList: $isParamEnabledList, bodyContentType: $bodyContentType, body: $body, query: $query, formData: $formData)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HttpRequestModelImpl && + (identical(other.method, method) || other.method == method) && + (identical(other.url, url) || other.url == url) && + const DeepCollectionEquality().equals(other._headers, _headers) && + const DeepCollectionEquality().equals(other._params, _params) && + const DeepCollectionEquality() + .equals(other._isHeaderEnabledList, _isHeaderEnabledList) && + const DeepCollectionEquality() + .equals(other._isParamEnabledList, _isParamEnabledList) && + (identical(other.bodyContentType, bodyContentType) || + other.bodyContentType == bodyContentType) && + (identical(other.body, body) || other.body == body) && + (identical(other.query, query) || other.query == query) && + const DeepCollectionEquality().equals(other._formData, _formData)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + method, + url, + const DeepCollectionEquality().hash(_headers), + const DeepCollectionEquality().hash(_params), + const DeepCollectionEquality().hash(_isHeaderEnabledList), + const DeepCollectionEquality().hash(_isParamEnabledList), + bodyContentType, + body, + query, + const DeepCollectionEquality().hash(_formData)); + + /// Create a copy of HttpRequestModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$HttpRequestModelImplCopyWith<_$HttpRequestModelImpl> get copyWith => + __$$HttpRequestModelImplCopyWithImpl<_$HttpRequestModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$HttpRequestModelImplToJson( + this, + ); + } +} + +abstract class _HttpRequestModel extends HttpRequestModel { + const factory _HttpRequestModel( + {final HTTPVerb method, + final String url, + final List? headers, + final List? params, + final List? isHeaderEnabledList, + final List? isParamEnabledList, + final ContentType bodyContentType, + final String? body, + final String? query, + final List? formData}) = _$HttpRequestModelImpl; + const _HttpRequestModel._() : super._(); + + factory _HttpRequestModel.fromJson(Map json) = + _$HttpRequestModelImpl.fromJson; + + @override + HTTPVerb get method; + @override + String get url; + @override + List? get headers; + @override + List? get params; + @override + List? get isHeaderEnabledList; + @override + List? get isParamEnabledList; + @override + ContentType get bodyContentType; + @override + String? get body; + @override + String? get query; + @override + List? get formData; + + /// Create a copy of HttpRequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$HttpRequestModelImplCopyWith<_$HttpRequestModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/better_networking/lib/models/http_request_model.g.dart b/packages/better_networking/lib/models/http_request_model.g.dart new file mode 100644 index 00000000..13795ded --- /dev/null +++ b/packages/better_networking/lib/models/http_request_model.g.dart @@ -0,0 +1,68 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'http_request_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$HttpRequestModelImpl _$$HttpRequestModelImplFromJson(Map json) => + _$HttpRequestModelImpl( + method: $enumDecodeNullable(_$HTTPVerbEnumMap, json['method']) ?? + HTTPVerb.get, + url: json['url'] as String? ?? "", + headers: (json['headers'] as List?) + ?.map((e) => + NameValueModel.fromJson(Map.from(e as Map))) + .toList(), + params: (json['params'] as List?) + ?.map((e) => + NameValueModel.fromJson(Map.from(e as Map))) + .toList(), + isHeaderEnabledList: (json['isHeaderEnabledList'] as List?) + ?.map((e) => e as bool) + .toList(), + isParamEnabledList: (json['isParamEnabledList'] as List?) + ?.map((e) => e as bool) + .toList(), + bodyContentType: + $enumDecodeNullable(_$ContentTypeEnumMap, json['bodyContentType']) ?? + ContentType.json, + body: json['body'] as String?, + query: json['query'] as String?, + formData: (json['formData'] as List?) + ?.map((e) => + FormDataModel.fromJson(Map.from(e as Map))) + .toList(), + ); + +Map _$$HttpRequestModelImplToJson( + _$HttpRequestModelImpl instance) => + { + 'method': _$HTTPVerbEnumMap[instance.method]!, + 'url': instance.url, + 'headers': instance.headers?.map((e) => e.toJson()).toList(), + 'params': instance.params?.map((e) => e.toJson()).toList(), + 'isHeaderEnabledList': instance.isHeaderEnabledList, + 'isParamEnabledList': instance.isParamEnabledList, + 'bodyContentType': _$ContentTypeEnumMap[instance.bodyContentType]!, + 'body': instance.body, + 'query': instance.query, + 'formData': instance.formData?.map((e) => e.toJson()).toList(), + }; + +const _$HTTPVerbEnumMap = { + HTTPVerb.get: 'get', + HTTPVerb.head: 'head', + HTTPVerb.post: 'post', + HTTPVerb.put: 'put', + HTTPVerb.patch: 'patch', + HTTPVerb.delete: 'delete', + HTTPVerb.options: 'options', +}; + +const _$ContentTypeEnumMap = { + ContentType.json: 'json', + ContentType.text: 'text', + ContentType.formdata: 'formdata', +}; diff --git a/packages/better_networking/lib/models/http_response_model.dart b/packages/better_networking/lib/models/http_response_model.dart new file mode 100644 index 00000000..914aaa57 --- /dev/null +++ b/packages/better_networking/lib/models/http_response_model.dart @@ -0,0 +1,88 @@ +import 'dart:io'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:collection/collection.dart' show mergeMaps; +import 'package:http/http.dart'; +import 'package:http_parser/http_parser.dart'; +import '../extensions/extensions.dart'; +import '../utils/utils.dart'; +import '../consts.dart'; + +part 'http_response_model.freezed.dart'; +part 'http_response_model.g.dart'; + +class Uint8ListConverter implements JsonConverter?> { + const Uint8ListConverter(); + + @override + Uint8List? fromJson(List? json) { + return json == null ? null : Uint8List.fromList(json); + } + + @override + List? toJson(Uint8List? object) { + return object?.toList(); + } +} + +class DurationConverter implements JsonConverter { + const DurationConverter(); + + @override + Duration? fromJson(int? json) { + return json == null ? null : Duration(microseconds: json); + } + + @override + int? toJson(Duration? object) { + return object?.inMicroseconds; + } +} + +@freezed +class HttpResponseModel with _$HttpResponseModel { + const HttpResponseModel._(); + + @JsonSerializable( + explicitToJson: true, + anyMap: true, + ) + const factory HttpResponseModel({ + int? statusCode, + Map? headers, + Map? requestHeaders, + String? body, + String? formattedBody, + @Uint8ListConverter() Uint8List? bodyBytes, + @DurationConverter() Duration? time, + }) = _HttpResponseModel; + + factory HttpResponseModel.fromJson(Map json) => + _$HttpResponseModelFromJson(json); + + String? get contentType => headers?.getValueContentType(); + MediaType? get mediaType => getMediaTypeFromHeaders(headers); + + HttpResponseModel fromResponse({ + required Response response, + Duration? time, + }) { + final responseHeaders = mergeMaps( + {HttpHeaders.contentLengthHeader: response.contentLength.toString()}, + response.headers); + MediaType? mediaType = getMediaTypeFromHeaders(responseHeaders); + final body = (mediaType?.subtype == kSubTypeJson) + ? utf8.decode(response.bodyBytes) + : response.body; + return HttpResponseModel( + statusCode: response.statusCode, + headers: responseHeaders, + requestHeaders: response.request?.headers, + body: body, + formattedBody: formatBody(body, mediaType), + bodyBytes: response.bodyBytes, + time: time, + ); + } +} diff --git a/packages/better_networking/lib/models/http_response_model.freezed.dart b/packages/better_networking/lib/models/http_response_model.freezed.dart new file mode 100644 index 00000000..45071986 --- /dev/null +++ b/packages/better_networking/lib/models/http_response_model.freezed.dart @@ -0,0 +1,327 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'http_response_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +HttpResponseModel _$HttpResponseModelFromJson(Map json) { + return _HttpResponseModel.fromJson(json); +} + +/// @nodoc +mixin _$HttpResponseModel { + int? get statusCode => throw _privateConstructorUsedError; + Map? get headers => throw _privateConstructorUsedError; + Map? get requestHeaders => throw _privateConstructorUsedError; + String? get body => throw _privateConstructorUsedError; + String? get formattedBody => throw _privateConstructorUsedError; + @Uint8ListConverter() + Uint8List? get bodyBytes => throw _privateConstructorUsedError; + @DurationConverter() + Duration? get time => throw _privateConstructorUsedError; + + /// Serializes this HttpResponseModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of HttpResponseModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $HttpResponseModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HttpResponseModelCopyWith<$Res> { + factory $HttpResponseModelCopyWith( + HttpResponseModel value, $Res Function(HttpResponseModel) then) = + _$HttpResponseModelCopyWithImpl<$Res, HttpResponseModel>; + @useResult + $Res call( + {int? statusCode, + Map? headers, + Map? requestHeaders, + String? body, + String? formattedBody, + @Uint8ListConverter() Uint8List? bodyBytes, + @DurationConverter() Duration? time}); +} + +/// @nodoc +class _$HttpResponseModelCopyWithImpl<$Res, $Val extends HttpResponseModel> + implements $HttpResponseModelCopyWith<$Res> { + _$HttpResponseModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of HttpResponseModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? statusCode = freezed, + Object? headers = freezed, + Object? requestHeaders = freezed, + Object? body = freezed, + Object? formattedBody = freezed, + Object? bodyBytes = freezed, + Object? time = freezed, + }) { + return _then(_value.copyWith( + statusCode: freezed == statusCode + ? _value.statusCode + : statusCode // ignore: cast_nullable_to_non_nullable + as int?, + headers: freezed == headers + ? _value.headers + : headers // ignore: cast_nullable_to_non_nullable + as Map?, + requestHeaders: freezed == requestHeaders + ? _value.requestHeaders + : requestHeaders // ignore: cast_nullable_to_non_nullable + as Map?, + body: freezed == body + ? _value.body + : body // ignore: cast_nullable_to_non_nullable + as String?, + formattedBody: freezed == formattedBody + ? _value.formattedBody + : formattedBody // ignore: cast_nullable_to_non_nullable + as String?, + bodyBytes: freezed == bodyBytes + ? _value.bodyBytes + : bodyBytes // ignore: cast_nullable_to_non_nullable + as Uint8List?, + time: freezed == time + ? _value.time + : time // ignore: cast_nullable_to_non_nullable + as Duration?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$HttpResponseModelImplCopyWith<$Res> + implements $HttpResponseModelCopyWith<$Res> { + factory _$$HttpResponseModelImplCopyWith(_$HttpResponseModelImpl value, + $Res Function(_$HttpResponseModelImpl) then) = + __$$HttpResponseModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int? statusCode, + Map? headers, + Map? requestHeaders, + String? body, + String? formattedBody, + @Uint8ListConverter() Uint8List? bodyBytes, + @DurationConverter() Duration? time}); +} + +/// @nodoc +class __$$HttpResponseModelImplCopyWithImpl<$Res> + extends _$HttpResponseModelCopyWithImpl<$Res, _$HttpResponseModelImpl> + implements _$$HttpResponseModelImplCopyWith<$Res> { + __$$HttpResponseModelImplCopyWithImpl(_$HttpResponseModelImpl _value, + $Res Function(_$HttpResponseModelImpl) _then) + : super(_value, _then); + + /// Create a copy of HttpResponseModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? statusCode = freezed, + Object? headers = freezed, + Object? requestHeaders = freezed, + Object? body = freezed, + Object? formattedBody = freezed, + Object? bodyBytes = freezed, + Object? time = freezed, + }) { + return _then(_$HttpResponseModelImpl( + statusCode: freezed == statusCode + ? _value.statusCode + : statusCode // ignore: cast_nullable_to_non_nullable + as int?, + headers: freezed == headers + ? _value._headers + : headers // ignore: cast_nullable_to_non_nullable + as Map?, + requestHeaders: freezed == requestHeaders + ? _value._requestHeaders + : requestHeaders // ignore: cast_nullable_to_non_nullable + as Map?, + body: freezed == body + ? _value.body + : body // ignore: cast_nullable_to_non_nullable + as String?, + formattedBody: freezed == formattedBody + ? _value.formattedBody + : formattedBody // ignore: cast_nullable_to_non_nullable + as String?, + bodyBytes: freezed == bodyBytes + ? _value.bodyBytes + : bodyBytes // ignore: cast_nullable_to_non_nullable + as Uint8List?, + time: freezed == time + ? _value.time + : time // ignore: cast_nullable_to_non_nullable + as Duration?, + )); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, anyMap: true) +class _$HttpResponseModelImpl extends _HttpResponseModel { + const _$HttpResponseModelImpl( + {this.statusCode, + final Map? headers, + final Map? requestHeaders, + this.body, + this.formattedBody, + @Uint8ListConverter() this.bodyBytes, + @DurationConverter() this.time}) + : _headers = headers, + _requestHeaders = requestHeaders, + super._(); + + factory _$HttpResponseModelImpl.fromJson(Map json) => + _$$HttpResponseModelImplFromJson(json); + + @override + final int? statusCode; + final Map? _headers; + @override + Map? get headers { + final value = _headers; + if (value == null) return null; + if (_headers is EqualUnmodifiableMapView) return _headers; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + final Map? _requestHeaders; + @override + Map? get requestHeaders { + final value = _requestHeaders; + if (value == null) return null; + if (_requestHeaders is EqualUnmodifiableMapView) return _requestHeaders; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + final String? body; + @override + final String? formattedBody; + @override + @Uint8ListConverter() + final Uint8List? bodyBytes; + @override + @DurationConverter() + final Duration? time; + + @override + String toString() { + return 'HttpResponseModel(statusCode: $statusCode, headers: $headers, requestHeaders: $requestHeaders, body: $body, formattedBody: $formattedBody, bodyBytes: $bodyBytes, time: $time)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HttpResponseModelImpl && + (identical(other.statusCode, statusCode) || + other.statusCode == statusCode) && + const DeepCollectionEquality().equals(other._headers, _headers) && + const DeepCollectionEquality() + .equals(other._requestHeaders, _requestHeaders) && + (identical(other.body, body) || other.body == body) && + (identical(other.formattedBody, formattedBody) || + other.formattedBody == formattedBody) && + const DeepCollectionEquality().equals(other.bodyBytes, bodyBytes) && + (identical(other.time, time) || other.time == time)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + statusCode, + const DeepCollectionEquality().hash(_headers), + const DeepCollectionEquality().hash(_requestHeaders), + body, + formattedBody, + const DeepCollectionEquality().hash(bodyBytes), + time); + + /// Create a copy of HttpResponseModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$HttpResponseModelImplCopyWith<_$HttpResponseModelImpl> get copyWith => + __$$HttpResponseModelImplCopyWithImpl<_$HttpResponseModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$HttpResponseModelImplToJson( + this, + ); + } +} + +abstract class _HttpResponseModel extends HttpResponseModel { + const factory _HttpResponseModel( + {final int? statusCode, + final Map? headers, + final Map? requestHeaders, + final String? body, + final String? formattedBody, + @Uint8ListConverter() final Uint8List? bodyBytes, + @DurationConverter() final Duration? time}) = _$HttpResponseModelImpl; + const _HttpResponseModel._() : super._(); + + factory _HttpResponseModel.fromJson(Map json) = + _$HttpResponseModelImpl.fromJson; + + @override + int? get statusCode; + @override + Map? get headers; + @override + Map? get requestHeaders; + @override + String? get body; + @override + String? get formattedBody; + @override + @Uint8ListConverter() + Uint8List? get bodyBytes; + @override + @DurationConverter() + Duration? get time; + + /// Create a copy of HttpResponseModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$HttpResponseModelImplCopyWith<_$HttpResponseModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/better_networking/lib/models/http_response_model.g.dart b/packages/better_networking/lib/models/http_response_model.g.dart new file mode 100644 index 00000000..186fd152 --- /dev/null +++ b/packages/better_networking/lib/models/http_response_model.g.dart @@ -0,0 +1,35 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'http_response_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$HttpResponseModelImpl _$$HttpResponseModelImplFromJson(Map json) => + _$HttpResponseModelImpl( + statusCode: (json['statusCode'] as num?)?.toInt(), + headers: (json['headers'] as Map?)?.map( + (k, e) => MapEntry(k as String, e as String), + ), + requestHeaders: (json['requestHeaders'] as Map?)?.map( + (k, e) => MapEntry(k as String, e as String), + ), + body: json['body'] as String?, + formattedBody: json['formattedBody'] as String?, + bodyBytes: + const Uint8ListConverter().fromJson(json['bodyBytes'] as List?), + time: const DurationConverter().fromJson((json['time'] as num?)?.toInt()), + ); + +Map _$$HttpResponseModelImplToJson( + _$HttpResponseModelImpl instance) => + { + 'statusCode': instance.statusCode, + 'headers': instance.headers, + 'requestHeaders': instance.requestHeaders, + 'body': instance.body, + 'formattedBody': instance.formattedBody, + 'bodyBytes': const Uint8ListConverter().toJson(instance.bodyBytes), + 'time': const DurationConverter().toJson(instance.time), + }; diff --git a/packages/better_networking/lib/models/models.dart b/packages/better_networking/lib/models/models.dart new file mode 100644 index 00000000..a33c6fdd --- /dev/null +++ b/packages/better_networking/lib/models/models.dart @@ -0,0 +1,2 @@ +export 'http_request_model.dart'; +export 'http_response_model.dart'; diff --git a/packages/better_networking/lib/services/http_client_manager.dart b/packages/better_networking/lib/services/http_client_manager.dart new file mode 100644 index 00000000..7b815413 --- /dev/null +++ b/packages/better_networking/lib/services/http_client_manager.dart @@ -0,0 +1,65 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:http/io_client.dart'; + +http.Client createHttpClientWithNoSSL() { + var ioClient = HttpClient() + ..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + return IOClient(ioClient); +} + +class HttpClientManager { + static final HttpClientManager _instance = HttpClientManager._internal(); + static const int _maxCancelledRequests = 100; + final Map _clients = {}; + final Set _cancelledRequests = {}; + + factory HttpClientManager() { + return _instance; + } + + HttpClientManager._internal(); + + http.Client createClient( + String requestId, { + bool noSSL = false, + }) { + final client = + (noSSL && !kIsWeb) ? createHttpClientWithNoSSL() : http.Client(); + _clients[requestId] = client; + return client; + } + + void cancelRequest(String? requestId) { + if (requestId != null && _clients.containsKey(requestId)) { + _clients[requestId]?.close(); + _clients.remove(requestId); + + _cancelledRequests.add(requestId); + if (_cancelledRequests.length > _maxCancelledRequests) { + _cancelledRequests.remove(_cancelledRequests.first); + } + } + } + + bool wasRequestCancelled(String requestId) { + return _cancelledRequests.contains(requestId); + } + + void removeCancelledRequest(String requestId) { + _cancelledRequests.remove(requestId); + } + + void closeClient(String requestId) { + if (_clients.containsKey(requestId)) { + _clients[requestId]?.close(); + _clients.remove(requestId); + } + } + + bool hasActiveClient(String requestId) { + return _clients.containsKey(requestId); + } +} diff --git a/packages/better_networking/lib/services/http_service.dart b/packages/better_networking/lib/services/http_service.dart new file mode 100644 index 00000000..1b05976a --- /dev/null +++ b/packages/better_networking/lib/services/http_service.dart @@ -0,0 +1,169 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:http/http.dart' as http; +import 'package:seed/seed.dart'; +import '../consts.dart'; +import '../extensions/extensions.dart'; +import '../models/models.dart'; +import '../utils/utils.dart'; +import 'http_client_manager.dart'; + +typedef HttpResponse = http.Response; + +final httpClientManager = HttpClientManager(); + +Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( + String requestId, + APIType apiType, + HttpRequestModel requestModel, { + SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, + bool noSSL = false, +}) async { + if (httpClientManager.wasRequestCancelled(requestId)) { + httpClientManager.removeCancelledRequest(requestId); + } + final client = httpClientManager.createClient(requestId, noSSL: noSSL); + + (Uri?, String?) uriRec = getValidRequestUri( + requestModel.url, + requestModel.enabledParams, + defaultUriScheme: defaultUriScheme, + ); + + if (uriRec.$1 != null) { + Uri requestUrl = uriRec.$1!; + Map headers = requestModel.enabledHeadersMap; + bool overrideContentType = false; + HttpResponse? response; + String? body; + try { + Stopwatch stopwatch = Stopwatch()..start(); + if (apiType == APIType.rest) { + var isMultiPartRequest = + requestModel.bodyContentType == ContentType.formdata; + + if (kMethodsWithBody.contains(requestModel.method)) { + var requestBody = requestModel.body; + if (requestBody != null && + !isMultiPartRequest && + requestBody.isNotEmpty) { + body = requestBody; + if (requestModel.hasContentTypeHeader) { + overrideContentType = true; + } else { + headers[HttpHeaders.contentTypeHeader] = + requestModel.bodyContentType.header; + } + } + if (isMultiPartRequest) { + var multiPartRequest = http.MultipartRequest( + requestModel.method.name.toUpperCase(), + requestUrl, + ); + multiPartRequest.headers.addAll(headers); + for (var formData in requestModel.formDataList) { + if (formData.type == FormDataType.text) { + multiPartRequest.fields.addAll({formData.name: formData.value}); + } else { + multiPartRequest.files.add( + await http.MultipartFile.fromPath( + formData.name, + formData.value, + ), + ); + } + } + http.StreamedResponse multiPartResponse = + await client.send(multiPartRequest); + + stopwatch.stop(); + http.Response convertedMultiPartResponse = + await convertStreamedResponse(multiPartResponse); + return (convertedMultiPartResponse, stopwatch.elapsed, null); + } + } + switch (requestModel.method) { + case HTTPVerb.get: + response = await client.get(requestUrl, headers: headers); + break; + case HTTPVerb.head: + response = await client.head(requestUrl, headers: headers); + break; + case HTTPVerb.post: + case HTTPVerb.put: + case HTTPVerb.patch: + case HTTPVerb.delete: + case HTTPVerb.options: + final request = prepareHttpRequest( + url: requestUrl, + method: requestModel.method.name.toUpperCase(), + headers: headers, + body: body, + overrideContentType: overrideContentType, + ); + final streamed = await client.send(request); + response = await http.Response.fromStream(streamed); + break; + } + } + if (apiType == APIType.graphql) { + var requestBody = getGraphQLBody(requestModel); + if (requestBody != null) { + var contentLength = utf8.encode(requestBody).length; + if (contentLength > 0) { + body = requestBody; + headers[HttpHeaders.contentLengthHeader] = contentLength.toString(); + if (!requestModel.hasContentTypeHeader) { + headers[HttpHeaders.contentTypeHeader] = ContentType.json.header; + } + } + } + response = await client.post( + requestUrl, + headers: headers, + body: body, + ); + } + stopwatch.stop(); + return (response, stopwatch.elapsed, null); + } catch (e) { + if (httpClientManager.wasRequestCancelled(requestId)) { + return (null, null, kMsgRequestCancelled); + } + return (null, null, e.toString()); + } finally { + httpClientManager.closeClient(requestId); + } + } else { + return (null, null, uriRec.$2); + } +} + +void cancelHttpRequest(String? requestId) { + httpClientManager.cancelRequest(requestId); +} + +http.Request prepareHttpRequest({ + required Uri url, + required String method, + required Map headers, + required String? body, + bool overrideContentType = false, +}) { + var request = http.Request(method, url); + if (headers.getValueContentType() != null) { + request.headers[HttpHeaders.contentTypeHeader] = + headers.getValueContentType()!; + if (!overrideContentType) { + headers.removeKeyContentType(); + } + } + if (body != null) { + request.body = body; + headers[HttpHeaders.contentLengthHeader] = + request.bodyBytes.length.toString(); + } + request.headers.addAll(headers); + return request; +} diff --git a/packages/better_networking/lib/services/services.dart b/packages/better_networking/lib/services/services.dart new file mode 100644 index 00000000..d155e9c7 --- /dev/null +++ b/packages/better_networking/lib/services/services.dart @@ -0,0 +1,2 @@ +export 'http_client_manager.dart'; +export 'http_service.dart'; diff --git a/packages/better_networking/lib/utils/content_type_utils.dart b/packages/better_networking/lib/utils/content_type_utils.dart new file mode 100644 index 00000000..ace8d715 --- /dev/null +++ b/packages/better_networking/lib/utils/content_type_utils.dart @@ -0,0 +1,54 @@ +import 'package:http_parser/http_parser.dart'; +import '../consts.dart'; +import '../extensions/extensions.dart'; + +ContentType? getContentTypeFromHeadersMap( + Map? kvMap, +) { + if (kvMap != null && kvMap.hasKeyContentType()) { + var val = getMediaTypeFromHeaders(kvMap); + return getContentTypeFromMediaType(val); + } + return null; +} + +MediaType? getMediaTypeFromHeaders(Map? headers) { + var contentType = headers?.getValueContentType(); + MediaType? mediaType = getMediaTypeFromContentType(contentType); + return mediaType; +} + +MediaType? getMediaTypeFromContentType(String? contentType) { + if (contentType != null) { + try { + MediaType mediaType = MediaType.parse(contentType); + return mediaType; + } catch (e) { + return null; + } + } + return null; +} + +ContentType? getContentTypeFromMediaType(MediaType? mediaType) { + if (mediaType != null) { + if (mediaType.subtype.contains(kSubTypeJson)) { + return ContentType.json; + } else if (mediaType.type == kTypeMultipart && + mediaType.subtype == kSubTypeFormData) { + return ContentType.formdata; + } + return ContentType.text; + } + return null; +} + +ContentType? getContentTypeFromContentTypeStr( + String? contentType, +) { + if (contentType != null) { + var val = getMediaTypeFromContentType(contentType); + return getContentTypeFromMediaType(val); + } + return null; +} diff --git a/packages/better_networking/lib/utils/graphql_utils.dart b/packages/better_networking/lib/utils/graphql_utils.dart new file mode 100644 index 00000000..afa874bf --- /dev/null +++ b/packages/better_networking/lib/utils/graphql_utils.dart @@ -0,0 +1,11 @@ +import '../consts.dart'; +import '../models/models.dart'; + +String? getGraphQLBody(HttpRequestModel httpRequestModel) { + if (httpRequestModel.hasQuery) { + return kJsonEncoder.convert({ + "query": httpRequestModel.query, + }); + } + return null; +} diff --git a/packages/better_networking/lib/utils/http_request_utils.dart b/packages/better_networking/lib/utils/http_request_utils.dart new file mode 100644 index 00000000..c175733a --- /dev/null +++ b/packages/better_networking/lib/utils/http_request_utils.dart @@ -0,0 +1,111 @@ +import 'package:better_networking/consts.dart'; +import 'package:seed/seed.dart'; +import '../models/models.dart'; +import 'graphql_utils.dart'; +import 'package:json5/json5.dart' as json5; + +Map? rowsToMap( + List? kvRows, { + bool isHeader = false, +}) { + if (kvRows == null) { + return null; + } + Map finalMap = {}; + for (var row in kvRows) { + if (row.name.trim() != "") { + String key = row.name; + if (isHeader) { + key = key.toLowerCase(); + } + finalMap[key] = row.value.toString(); + } + } + return finalMap; +} + +List? mapToRows(Map? kvMap) { + if (kvMap == null) { + return null; + } + List finalRows = []; + for (var k in kvMap.keys) { + finalRows.add(NameValueModel(name: k, value: kvMap[k])); + } + return finalRows; +} + +List>? rowsToFormDataMapList(List? kvRows) { + if (kvRows == null) { + return null; + } + List> finalMap = kvRows + .map( + (FormDataModel formData) => + (formData.name.trim().isEmpty && formData.value.trim().isEmpty) + ? null + : { + "name": formData.name, + "value": formData.value, + "type": formData.type.name, + }, + ) + .nonNulls + .toList(); + return finalMap; +} + +List? mapListToFormDataModelRows(List? kvMap) { + if (kvMap == null) { + return null; + } + List finalRows = kvMap.map((formData) { + return FormDataModel( + name: formData["name"], + value: formData["value"], + type: getFormDataType(formData["type"]), + ); + }).toList(); + return finalRows; +} + +FormDataType getFormDataType(String? type) { + return FormDataType.values.firstWhere( + (element) => element.name == type, + orElse: () => FormDataType.text, + ); +} + +List? getEnabledRows( + List? rows, + List? isRowEnabledList, +) { + if (rows == null || isRowEnabledList == null) { + return rows; + } + List finalRows = rows + .where((element) => isRowEnabledList[rows.indexOf(element)]) + .toList(); + return finalRows == [] ? null : finalRows; +} + +String? getRequestBody(APIType type, HttpRequestModel httpRequestModel) { + return switch (type) { + APIType.rest => + (httpRequestModel.hasJsonData || httpRequestModel.hasTextData) + ? httpRequestModel.body + : null, + APIType.graphql => getGraphQLBody(httpRequestModel), + }; +} + +// TODO: Expose this function to remove JSON comments +String? removeJsonComments(String? json) { + try { + if (json == null) return null; + var parsed = json5.json5Decode(json); + return kJsonEncoder.convert(parsed); + } catch (e) { + return json; + } +} diff --git a/packages/better_networking/lib/utils/http_response_utils.dart b/packages/better_networking/lib/utils/http_response_utils.dart new file mode 100644 index 00000000..c5ebfda1 --- /dev/null +++ b/packages/better_networking/lib/utils/http_response_utils.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:http/http.dart' as http; +import 'package:http_parser/http_parser.dart'; +import 'package:xml/xml.dart'; +import '../consts.dart'; + +String? formatBody(String? body, MediaType? mediaType) { + if (mediaType != null && body != null) { + var subtype = mediaType.subtype; + try { + if (subtype.contains(kSubTypeJson)) { + final tmp = jsonDecode(body); + String result = kJsonEncoder.convert(tmp); + return result; + } + if (subtype.contains(kSubTypeXml)) { + final document = XmlDocument.parse(body); + String result = document.toXmlString(pretty: true, indent: ' '); + return result; + } + if (subtype == kSubTypeHtml) { + var len = body.length; + var lines = kSplitter.convert(body); + var numOfLines = lines.length; + if (numOfLines != 0 && len / numOfLines <= kCodeCharsPerLineLimit) { + return body; + } + } + } catch (e) { + return null; + } + } + return null; +} + +Future convertStreamedResponse( + http.StreamedResponse streamedResponse, +) async { + Uint8List bodyBytes = await streamedResponse.stream.toBytes(); + + http.Response response = http.Response.bytes( + bodyBytes, + streamedResponse.statusCode, + headers: streamedResponse.headers, + persistentConnection: streamedResponse.persistentConnection, + reasonPhrase: streamedResponse.reasonPhrase, + request: streamedResponse.request, + ); + + return response; +} diff --git a/packages/better_networking/lib/utils/string_utils.dart b/packages/better_networking/lib/utils/string_utils.dart new file mode 100644 index 00000000..3f5c3fc1 --- /dev/null +++ b/packages/better_networking/lib/utils/string_utils.dart @@ -0,0 +1,19 @@ +import 'dart:math'; + +class RandomStringGenerator { + static const _chars = + 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; + static Random rnd = Random(); + + static String getRandomString(int length) => + String.fromCharCodes(Iterable.generate( + length, (_) => _chars.codeUnitAt(rnd.nextInt(_chars.length)))); + + static String getRandomStringLines(int lines, int length) { + List result = []; + for (var i = 0; i < lines; i++) { + result.add(getRandomString(length)); + } + return result.join('\n'); + } +} diff --git a/packages/better_networking/lib/utils/uri_utils.dart b/packages/better_networking/lib/utils/uri_utils.dart new file mode 100644 index 00000000..a330e582 --- /dev/null +++ b/packages/better_networking/lib/utils/uri_utils.dart @@ -0,0 +1,65 @@ +import 'package:collection/collection.dart' show mergeMaps; +import 'package:seed/seed.dart'; +import '../consts.dart'; +import 'http_request_utils.dart'; + +(String?, bool) getUriScheme(Uri uri) { + if (uri.hasScheme) { + if (kSupportedUriSchemes.contains(uri.scheme.toLowerCase())) { + return (uri.scheme, true); + } + return (uri.scheme, false); + } + return (null, false); +} + +String stripUriParams(Uri uri) { + return "${uri.scheme}://${uri.authority}${uri.path}"; +} + +String stripUrlParams(String url) { + var idx = url.indexOf("?"); + return idx > 0 ? url.substring(0, idx) : url; +} + +(Uri?, String?) getValidRequestUri( + String? url, List? requestParams, + {SupportedUriSchemes defaultUriScheme = kDefaultUriScheme}) { + url = url?.trim(); + if (url == null || url == "") { + return (null, "URL is missing!"); + } + + if (kLocalhostRegex.hasMatch(url) || kIPHostRegex.hasMatch(url)) { + url = '${SupportedUriSchemes.http.name}://$url'; + } + + Uri? uri = Uri.tryParse(url); + if (uri == null) { + return (null, "Check URL (malformed)"); + } + (String?, bool) urlScheme = getUriScheme(uri); + + if (urlScheme.$1 != null) { + if (!urlScheme.$2) { + return (null, "Unsupported URL Scheme (${urlScheme.$1})"); + } + } else { + url = "${defaultUriScheme.name}://$url"; + } + + uri = Uri.parse(url); + if (uri.hasFragment) { + uri = uri.removeFragment(); + } + + Map? queryParams = rowsToMap(requestParams); + if (queryParams != null && queryParams.isNotEmpty) { + if (uri.hasQuery) { + Map urlQueryParams = uri.queryParameters; + queryParams = mergeMaps(urlQueryParams, queryParams); + } + uri = uri.replace(queryParameters: queryParams); + } + return (uri, null); +} diff --git a/packages/better_networking/lib/utils/utils.dart b/packages/better_networking/lib/utils/utils.dart new file mode 100644 index 00000000..f39e563d --- /dev/null +++ b/packages/better_networking/lib/utils/utils.dart @@ -0,0 +1,6 @@ +export 'content_type_utils.dart'; +export 'graphql_utils.dart'; +export 'http_request_utils.dart'; +export 'http_response_utils.dart'; +export 'string_utils.dart'; +export 'uri_utils.dart'; diff --git a/packages/better_networking/pubspec.yaml b/packages/better_networking/pubspec.yaml new file mode 100644 index 00000000..151e1a5b --- /dev/null +++ b/packages/better_networking/pubspec.yaml @@ -0,0 +1,27 @@ +name: better_networking +description: "Simplified Networking" +version: 0.0.1 +homepage: + +environment: + sdk: ^3.8.0 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + freezed_annotation: ^2.4.1 + http: ^1.3.0 + http_parser: ^4.1.2 + json5: ^0.8.2 + seed: ^0.0.3 + xml: ^6.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.12 + flutter_lints: ^4.0.0 + freezed: ^2.5.7 + json_serializable: ^6.7.1 + test: ^1.25.2 diff --git a/packages/better_networking/test/better_networking_test.dart b/packages/better_networking/test/better_networking_test.dart new file mode 100644 index 00000000..a5ef434a --- /dev/null +++ b/packages/better_networking/test/better_networking_test.dart @@ -0,0 +1,5 @@ +// ignore_for_file: unused_import + +import 'package:flutter_test/flutter_test.dart'; + +void main() {} diff --git a/packages/better_networking/test/extensions/map_extensions_test.dart b/packages/better_networking/test/extensions/map_extensions_test.dart new file mode 100644 index 00000000..7de036c0 --- /dev/null +++ b/packages/better_networking/test/extensions/map_extensions_test.dart @@ -0,0 +1,127 @@ +import 'package:better_networking/extensions/extensions.dart'; +import 'package:test/test.dart'; + +void main() { + group('Testing MapExtensions', () { + group('Testing hasKeyContentType()', () { + test('Content-Type present should return true', () { + Map mapEx = {"Content-Type": "x", "Agent": "Test"}; + expect(mapEx.hasKeyContentType(), true); + }); + + test('content-Type present should return true', () { + Map mapEx = {"content-Type": "x", "Agent": "Test"}; + expect(mapEx.hasKeyContentType(), true); + }); + + test('empty should return false', () { + Map mapEx = {}; + expect(mapEx.hasKeyContentType(), false); + }); + + test('No content-type present should return false', () { + Map mapEx = {"Agent": "Test"}; + expect(mapEx.hasKeyContentType(), false); + }); + + test('Different datatype should return false', () { + Map mapEx = {1: "Test"}; + expect(mapEx.hasKeyContentType(), false); + }); + + test('Mixed datatype but should return true', () { + Map mapEx = {1: "Test", "content-type": "x"}; + expect(mapEx.hasKeyContentType(), true); + }); + }); + + group('Testing getKeyContentType()', () { + test('Content-Type present', () { + Map mapEx = {"Agent": "Test", "Content-Type": "x"}; + expect(mapEx.getKeyContentType(), "Content-Type"); + }); + + test('content-Type present', () { + Map mapEx = {"Agent": "Test", "content-Type": "x"}; + expect(mapEx.getKeyContentType(), "content-Type"); + }); + + test('empty should return null', () { + Map mapEx = {}; + expect(mapEx.getKeyContentType(), null); + }); + + test('No content-type present should return null', () { + Map mapEx = {"Agent": "Test"}; + expect(mapEx.getKeyContentType(), null); + }); + + test('Different datatype should return null', () { + Map mapEx = {1: "Test"}; + expect(mapEx.getKeyContentType(), null); + }); + + test('Mixed datatype but should return content-type', () { + Map mapEx = {1: "Test", "content-type": "x"}; + expect(mapEx.getKeyContentType(), "content-type"); + }); + + test('Multiple occurence should return first', () { + Map mapEx = {1: "Test", "content-Type": "y", "content-type": "x"}; + expect(mapEx.getKeyContentType(), "content-Type"); + }); + }); + }); + + group('Testing getValueContentType()', () { + test('Content-Type present', () { + Map mapEx = {"Agent": "Test", "Content-Type": "x"}; + expect(mapEx.getValueContentType(), "x"); + }); + + test('content-Type present', () { + Map mapEx = {"Agent": "Test", "content-Type": "x"}; + expect(mapEx.getValueContentType(), "x"); + }); + + test('empty should return null', () { + Map mapEx = {}; + expect(mapEx.getValueContentType(), null); + }); + + test('No content-type present should return null', () { + Map mapEx = {"Agent": "Test"}; + expect(mapEx.getValueContentType(), null); + }); + + test('Different datatype should return null', () { + Map mapEx = {1: "Test"}; + expect(mapEx.getValueContentType(), null); + }); + + test('Mixed datatype but should return x', () { + Map mapEx = {1: "Test", "content-type": "x"}; + expect(mapEx.getValueContentType(), "x"); + }); + + test('Multiple occurence should return first', () { + Map mapEx = {1: "Test", "content-Type": "y", "content-type": "x"}; + expect(mapEx.getValueContentType(), "y"); + }); + }); + + group("Testing ?.getValueContentType() function", () { + test('Testing ?.getValueContentType() for header1', () { + Map header1 = {"content-type": "application/json"}; + String contentType1Expected = "application/json"; + expect(header1.getValueContentType(), contentType1Expected); + }); + test( + 'Testing ?.getValueContentType() when header keys are in header case', + () { + Map header2 = {"Content-Type": "application/json"}; + expect(header2.getValueContentType(), "application/json"); + }, + ); + }); +} diff --git a/packages/better_networking/test/utils/http_request_utils.dart b/packages/better_networking/test/utils/http_request_utils.dart new file mode 100644 index 00000000..1356bdb9 --- /dev/null +++ b/packages/better_networking/test/utils/http_request_utils.dart @@ -0,0 +1,74 @@ +import 'package:better_networking/utils/http_request_utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('Testing RemoveJsonComments', () { + test('Removes single-line comments', () { + String input = ''' + { + // This is a single-line comment + "key": "value" + } + '''; + + String expected = '''{ + "key": "value" +}'''; + expect(removeJsonComments(input), expected); + }); + + test('Removes multi-line comments', () { + String input = ''' + { + /* + This is a multi-line comment + */ + "key": "value" + } + '''; + + String expected = '''{ + "key": "value" +}'''; + expect(removeJsonComments(input), expected); + }); + + test('Handles valid JSON without comments', () { + String input = '{"key":"value"}'; + String expected = '''{ + "key": "value" +}'''; + expect(removeJsonComments(input), expected); + }); + + test('Returns original string if invalid JSON', () { + String input = '{key: value}'; + String expected = '{key: value}'; + expect(removeJsonComments(input), expected); + }); + + test('Removes trailing commas', () { + String input = ''' + { + "key1": "value1", + "key2": "value2", // trailing comma + } + '''; + + String expected = '''{ + "key1": "value1", + "key2": "value2" +}'''; + expect(removeJsonComments(input), expected); + }); + + test('Test blank json', () { + String input = ''' + {} + '''; + + String expected = '{}'; + expect(removeJsonComments(input), expected); + }); + }); +} diff --git a/packages/better_networking/test/utils/http_response_utils.dart b/packages/better_networking/test/utils/http_response_utils.dart new file mode 100644 index 00000000..4b14d52a --- /dev/null +++ b/packages/better_networking/test/utils/http_response_utils.dart @@ -0,0 +1,184 @@ +import 'package:better_networking/utils/content_type_utils.dart'; +import 'package:better_networking/utils/http_response_utils.dart'; +import 'package:better_networking/utils/string_utils.dart'; +import 'package:http_parser/http_parser.dart'; +import 'package:test/test.dart'; + +void main() { + group('Testing getMediaTypeFromContentType function', () { + test('Testing getMediaTypeFromContentType for json type', () { + String contentType1 = "application/json"; + MediaType mediaType1Expected = MediaType("application", "json"); + expect( + getMediaTypeFromContentType(contentType1).toString(), + mediaType1Expected.toString(), + ); + }); + test('Testing getMediaTypeFromContentType for null', () { + expect(getMediaTypeFromContentType(null), null); + }); + test('Testing getMediaTypeFromContentType for image svg+xml type', () { + String contentType3 = "image/svg+xml"; + MediaType mediaType3Expected = MediaType("image", "svg+xml"); + expect( + getMediaTypeFromContentType(contentType3).toString(), + mediaType3Expected.toString(), + ); + }); + test('Testing getMediaTypeFromContentType for incorrect content type', () { + String contentType4 = "text/html : charset=utf-8"; + expect(getMediaTypeFromContentType(contentType4), null); + }); + test('Testing getMediaTypeFromContentType for text/css type', () { + String contentType5 = "text/css; charset=utf-8"; + MediaType mediaType5Expected = MediaType("text", "css", { + "charset": "utf-8", + }); + expect( + getMediaTypeFromContentType(contentType5).toString(), + mediaType5Expected.toString(), + ); + }); + test('Testing getMediaTypeFromContentType for incorrect with double ;', () { + String contentType6 = + "application/xml; charset=utf-16be ; date=21/03/2023"; + expect(getMediaTypeFromContentType(contentType6), null); + }); + test('Testing getMediaTypeFromContentType for empty content type', () { + expect(getMediaTypeFromContentType(""), null); + }); + test('Testing getMediaTypeFromContentType for missing subtype', () { + String contentType7 = "audio"; + expect(getMediaTypeFromContentType(contentType7), null); + }); + test('Testing getMediaTypeFromContentType for missing Type', () { + String contentType8 = "/html"; + expect(getMediaTypeFromContentType(contentType8), null); + }); + }); + + group("Testing getMediaTypeFromHeaders", () { + test('Testing getMediaTypeFromHeaders for basic case', () { + Map header1 = { + "content-length": "4506", + "cache-control": "private", + "content-type": "application/json", + }; + MediaType mediaType1Expected = MediaType("application", "json"); + expect( + getMediaTypeFromHeaders(header1).toString(), + mediaType1Expected.toString(), + ); + }); + test('Testing getMediaTypeFromHeaders for null header', () { + expect(getMediaTypeFromHeaders(null), null); + }); + test('Testing getMediaTypeFromHeaders for incomplete header value', () { + Map header2 = {"content-length": "4506"}; + expect(getMediaTypeFromHeaders(header2), null); + }); + test('Testing getMediaTypeFromHeaders for empty header value', () { + Map header3 = {"content-type": ""}; + expect(getMediaTypeFromHeaders(header3), null); + }); + test( + 'Testing getMediaTypeFromHeaders for erroneous header value - missing type', + () { + Map header4 = {"content-type": "/json"}; + expect(getMediaTypeFromHeaders(header4), null); + }, + ); + test( + 'Testing getMediaTypeFromHeaders for erroneous header value - missing subtype', + () { + Map header5 = {"content-type": "application"}; + expect(getMediaTypeFromHeaders(header5), null); + }, + ); + test('Testing getMediaTypeFromHeaders for header6', () { + Map header6 = {"content-type": "image/svg+xml"}; + MediaType mediaType6Expected = MediaType("image", "svg+xml"); + expect( + getMediaTypeFromHeaders(header6).toString(), + mediaType6Expected.toString(), + ); + }); + }); + + group("Testing formatBody", () { + test('Testing formatBody for null values', () { + expect(formatBody(null, null), null); + }); + test('Testing formatBody for null body values', () { + MediaType mediaType1 = MediaType("application", "xml"); + expect(formatBody(null, mediaType1), null); + }); + test('Testing formatBody for null MediaType values', () { + String body1 = ''' + { + "text":"The Chosen One"; + } +'''; + expect(formatBody(body1, null), null); + }); + test('Testing formatBody for json subtype values', () { + String body2 = '''{"data":{"area":9831510.0,"population":331893745}}'''; + MediaType mediaType2 = MediaType("application", "json"); + String result2Expected = '''{ + "data": { + "area": 9831510.0, + "population": 331893745 + } +}'''; + expect(formatBody(body2, mediaType2), result2Expected); + }); + test('Testing formatBody for xml subtype values', () { + String body3 = ''' + + +Belgian Waffles +5.95 USD +Two of our famous Belgian Waffles with plenty of real maple syrup +650 + + +'''; + MediaType mediaType3 = MediaType("application", "xml"); + String result3Expected = ''' + + Belgian Waffles + 5.95 USD + Two of our famous Belgian Waffles with plenty of real maple syrup + 650 + +'''; + expect(formatBody(body3, mediaType3), result3Expected); + }); + group("Testing formatBody for html", () { + MediaType mediaTypeHtml = MediaType("text", "html"); + test('Testing formatBody for html subtype values', () { + String body4 = ''' + +

My First Heading

+

My first paragraph.

+ +'''; + expect(formatBody(body4, mediaTypeHtml), body4); + }); + + test('Testing formatBody for html subtype values with random values', () { + String body5 = + '''${RandomStringGenerator.getRandomStringLines(100, 10000)}'''; + expect(formatBody(body5, mediaTypeHtml), null); + }); + test( + 'Testing formatBody for html subtype values with random values within limit', + () { + String body6 = + '''${RandomStringGenerator.getRandomStringLines(100, 190)}'''; + expect(formatBody(body6, mediaTypeHtml), body6); + }, + ); + }); + }); +} diff --git a/packages/better_networking/test/utils/uri_utils_test.dart b/packages/better_networking/test/utils/uri_utils_test.dart new file mode 100644 index 00000000..6d68d9e3 --- /dev/null +++ b/packages/better_networking/test/utils/uri_utils_test.dart @@ -0,0 +1,180 @@ +import 'package:better_networking/utils/uri_utils.dart'; +import 'package:seed/seed.dart'; +import 'package:test/test.dart'; + +void main() { + group("Testing getUriScheme", () { + test('Testing getUriScheme for https', () { + Uri uri1 = Uri( + scheme: 'https', + host: 'dart.dev', + path: 'guides/libraries/library-tour', + fragment: 'numbers', + ); + String uriScheme1Expected = 'https'; + expect(getUriScheme(uri1), (uriScheme1Expected, true)); + }); + test('Testing getUriScheme for mailto scheme value', () { + Uri uri2 = Uri(scheme: 'mailto'); + String uriScheme2Expected = 'mailto'; + expect(getUriScheme(uri2), (uriScheme2Expected, false)); + }); + test('Testing getUriScheme for empty scheme value', () { + Uri uri3 = Uri(scheme: ''); + expect(getUriScheme(uri3), (null, false)); + }); + test('Testing getUriScheme for null scheme value', () { + Uri uri4 = Uri(scheme: null); + expect(getUriScheme(uri4), (null, false)); + }); + }); + + group("Testing getValidRequestUri", () { + test( + 'Testing getValidRequestUri with localhost URL without port or path', + () { + String url1 = "localhost"; + Uri uri1Expected = Uri(scheme: 'http', host: 'localhost'); + expect(getValidRequestUri(url1, []), (uri1Expected, null)); + }, + ); + + test('Testing getValidRequestUri with localhost URL with port', () { + String url1 = "localhost:8080"; + Uri uri1Expected = Uri(scheme: 'http', host: 'localhost', port: 8080); + expect(getValidRequestUri(url1, []), (uri1Expected, null)); + }); + + test( + 'Testing getValidRequestUri with localhost URL with port and path', + () { + String url1 = "localhost:8080/hello"; + Uri uri1Expected = Uri( + scheme: 'http', + host: 'localhost', + port: 8080, + path: '/hello', + ); + expect(getValidRequestUri(url1, []), (uri1Expected, null)); + }, + ); + + test('Testing getValidRequestUri with localhost URL with http prefix', () { + String url1 = "http://localhost:3080"; + Uri uri1Expected = Uri(scheme: 'http', host: 'localhost', port: 3080); + expect(getValidRequestUri(url1, []), (uri1Expected, null)); + }); + + test('Testing getValidRequestUri with localhost URL with https prefix', () { + String url1 = "https://localhost:8080"; + Uri uri1Expected = Uri(scheme: 'https', host: 'localhost', port: 8080); + expect(getValidRequestUri(url1, []), (uri1Expected, null)); + }); + + test('Testing getValidRequestUri with IP URL without port or path', () { + String url1 = "8.8.8.8"; + Uri uri1Expected = Uri(scheme: 'http', host: '8.8.8.8'); + expect(getValidRequestUri(url1, []), (uri1Expected, null)); + }); + + test('Testing getValidRequestUri with IP URL with port', () { + String url1 = "8.8.8.8:8080"; + Uri uri1Expected = Uri(scheme: 'http', host: '8.8.8.8', port: 8080); + expect(getValidRequestUri(url1, []), (uri1Expected, null)); + }); + + test('Testing getValidRequestUri with IP URL with port and path', () { + String url1 = "8.8.8.8:8080/hello"; + Uri uri1Expected = Uri( + scheme: 'http', + host: '8.8.8.8', + port: 8080, + path: '/hello', + ); + expect(getValidRequestUri(url1, []), (uri1Expected, null)); + }); + + test('Testing getValidRequestUri with IP URL with http prefix', () { + String url1 = "http://8.8.8.8:3080"; + Uri uri1Expected = Uri(scheme: 'http', host: '8.8.8.8', port: 3080); + expect(getValidRequestUri(url1, []), (uri1Expected, null)); + }); + + test('Testing getValidRequestUri with IP URL with https prefix', () { + String url1 = "https://8.8.8.8:8080"; + Uri uri1Expected = Uri(scheme: 'https', host: '8.8.8.8', port: 8080); + expect(getValidRequestUri(url1, []), (uri1Expected, null)); + }); + + test('Testing getValidRequestUri for normal values', () { + String url1 = "https://api.apidash.dev/country/data"; + const kvRow1 = NameValueModel(name: "code", value: "US"); + Uri uri1Expected = Uri( + scheme: 'https', + host: 'api.apidash.dev', + path: 'country/data', + queryParameters: {'code': 'US'}, + ); + expect(getValidRequestUri(url1, [kvRow1]), (uri1Expected, null)); + }); + test('Testing getValidRequestUri for null url value', () { + const kvRow2 = NameValueModel(name: "code", value: "US"); + expect(getValidRequestUri(null, [kvRow2]), (null, "URL is missing!")); + }); + test('Testing getValidRequestUri for empty url value', () { + const kvRow3 = NameValueModel(name: "", value: ""); + expect(getValidRequestUri("", [kvRow3]), (null, "URL is missing!")); + }); + test('Testing getValidRequestUri when https is not provided in url', () { + String url4 = "api.apidash.dev/country/data"; + const kvRow4 = NameValueModel(name: "code", value: "US"); + Uri uri4Expected = Uri( + scheme: 'https', + host: 'api.apidash.dev', + path: 'country/data', + queryParameters: {'code': 'US'}, + ); + expect(getValidRequestUri(url4, [kvRow4]), (uri4Expected, null)); + }); + test('Testing getValidRequestUri when url has fragment', () { + String url5 = "https://dart.dev/guides/libraries/library-tour#numbers"; + Uri uri5Expected = Uri( + scheme: 'https', + host: 'dart.dev', + path: '/guides/libraries/library-tour', + ); + expect(getValidRequestUri(url5, null), (uri5Expected, null)); + }); + test('Testing getValidRequestUri when uri scheme is not supported', () { + String url5 = "mailto:someone@example.com"; + expect(getValidRequestUri(url5, null), ( + null, + "Unsupported URL Scheme (mailto)", + )); + }); + test( + 'Testing getValidRequestUri when query params in both url and kvrow', + () { + String url6 = "api.apidash.dev/country/data?code=IND"; + const kvRow6 = NameValueModel(name: "code", value: "US"); + Uri uri6Expected = Uri( + scheme: 'https', + host: 'api.apidash.dev', + path: 'country/data', + queryParameters: {'code': 'US'}, + ); + expect(getValidRequestUri(url6, [kvRow6]), (uri6Expected, null)); + }, + ); + test('Testing getValidRequestUri when kvrow is null', () { + String url7 = "api.apidash.dev/country/data?code=US"; + Uri uri7Expected = Uri( + scheme: 'https', + host: 'api.apidash.dev', + path: 'country/data', + queryParameters: {'code': 'US'}, + ); + expect(getValidRequestUri(url7, null), (uri7Expected, null)); + }); + }); +}