GitHostApis: Use Result class

Instead of throwing exceptions. There are so so many edge cases which
are failing.
This commit is contained in:
Vishesh Handa
2021-06-12 14:27:33 +02:00
parent bec866ed69
commit 408c40b3af
7 changed files with 137 additions and 84 deletions

View File

@ -16,22 +16,32 @@ class GitHubFake implements GitHost {
Future launchOAuthScreen() async {}
@override
Future<UserInfo?> getUserInfo() async {}
@override
Future addDeployKey(String sshPublicKey, String repoFullName) async {}
@override
Future<GitHostRepo> createRepo(String name) async {
return GitHub.repoFromJson({});
Future<Result<UserInfo>> getUserInfo() async {
var ex = Exception("Not Implemented");
return Result.fail(ex);
}
@override
Future<GitHostRepo> getRepo(String name) async {
return GitHub.repoFromJson({});
Future<Result<void>> addDeployKey(
String sshPublicKey, String repoFullName) async {
var ex = Exception("Not Implemented");
return Result.fail(ex);
}
@override
Future<List<GitHostRepo>> listRepos() async {
Future<Result<GitHostRepo>> createRepo(String name) async {
var ex = Exception("Not Implemented");
return Result.fail(ex);
}
@override
Future<Result<GitHostRepo>> getRepo(String name) async {
var ex = Exception("Not Implemented");
return Result.fail(ex);
}
@override
Future<Result<List<GitHostRepo>>> listRepos() async {
List<dynamic> list = jsonDecode(data);
var repos = <GitHostRepo>[];
list.forEach((dynamic d) {
@ -40,6 +50,6 @@ class GitHubFake implements GitHost {
repos.add(repo);
});
return repos;
return Result(repos);
}
}

View File

@ -1,6 +1,9 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:dart_git/utils/result.dart';
export 'package:dart_git/utils/result.dart';
typedef OAuthCallback = void Function(GitHostException?);
@ -8,11 +11,11 @@ abstract class GitHost {
void init(OAuthCallback oAuthCallback);
Future launchOAuthScreen();
Future<UserInfo?> getUserInfo();
Future<List<GitHostRepo>> listRepos();
Future<GitHostRepo> createRepo(String name);
Future<GitHostRepo> getRepo(String name);
Future addDeployKey(String sshPublicKey, String repoFullName);
Future<Result<UserInfo>> getUserInfo();
Future<Result<List<GitHostRepo>>> listRepos();
Future<Result<GitHostRepo>> createRepo(String name);
Future<Result<GitHostRepo>> getRepo(String name);
Future<Result<void>> addDeployKey(String sshPublicKey, String repoFullName);
}
class UserInfo {
@ -99,6 +102,8 @@ class GitHostException implements Exception {
static const CreateRepoFailed = GitHostException("CreateRepoFailed");
static const DeployKeyFailed = GitHostException("DeployKeyFailed");
static const GetRepoFailed = GitHostException("GetRepoFailed");
static const HttpResponseFail = GitHostException("HttpResponseFail");
static const JsonDecodingFail = GitHostException("JsonDecodingFail");
final String cause;
const GitHostException(this.cause);

View File

@ -11,6 +11,8 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:gitjournal/utils/logger.dart';
import 'githost.dart';
// FIXME: Handle for edge cases of json.decode
class GitHub implements GitHost {
static const _clientID = "aa3072cbfb02b1db14ed";
static const _clientSecret = "010d303ea99f82330f2b228977cef9ddbf7af2cd";
@ -79,9 +81,10 @@ class GitHub implements GitHost {
}
@override
Future<List<GitHostRepo>> listRepos() async {
Future<Result<List<GitHostRepo>>> listRepos() async {
if (_accessCode.isEmpty) {
throw GitHostException.MissingAccessCode;
var ex = GitHostException.MissingAccessCode;
return Result.fail(ex);
}
var url =
@ -96,11 +99,12 @@ class GitHub implements GitHost {
var response = await http.get(url, headers: headers);
if (response.statusCode != 200) {
Log.d("Github listRepos: Invalid response " +
Log.e("Github listRepos: Invalid response " +
response.statusCode.toString() +
": " +
response.body);
return [];
var ex = GitHostException.HttpResponseFail;
return Result.fail(ex);
}
List<dynamic> list = jsonDecode(response.body);
@ -112,13 +116,14 @@ class GitHub implements GitHost {
});
// FIXME: Sort these based on some criteria
return repos;
return Result(repos);
}
@override
Future<GitHostRepo> createRepo(String name) async {
Future<Result<GitHostRepo>> createRepo(String name) async {
if (_accessCode.isEmpty) {
throw GitHostException.MissingAccessCode;
var ex = GitHostException.MissingAccessCode;
return Result.fail(ex);
}
var url = Uri.parse("https://api.github.com/user/repos");
@ -135,35 +140,39 @@ class GitHub implements GitHost {
var response =
await http.post(url, headers: headers, body: json.encode(data));
if (response.statusCode != 201) {
Log.d("Github createRepo: Invalid response " +
Log.e("Github createRepo: Invalid response " +
response.statusCode.toString() +
": " +
response.body);
if (response.statusCode == 422) {
if (response.body.contains("name already exists")) {
throw GitHostException.RepoExists;
var ex = GitHostException.RepoExists;
return Result.fail(ex);
}
}
throw GitHostException.CreateRepoFailed;
var ex = GitHostException.CreateRepoFailed;
return Result.fail(ex);
}
Log.d("GitHub createRepo: " + response.body);
Map<String, dynamic> map = json.decode(response.body);
return repoFromJson(map);
return Result(repoFromJson(map));
}
@override
Future<GitHostRepo> getRepo(String name) async {
Future<Result<GitHostRepo>> getRepo(String name) async {
if (_accessCode.isEmpty) {
throw GitHostException.MissingAccessCode;
var ex = GitHostException.MissingAccessCode;
return Result.fail(ex);
}
var userInfo = await getUserInfo();
if (userInfo == null) {
throw Exception("GitHub UserInfo not found. This is bad");
var userInfoR = await getUserInfo();
if (userInfoR.isFailure) {
return fail(userInfoR);
}
var userInfo = userInfoR.getOrThrow();
var owner = userInfo.username;
var url = Uri.parse("https://api.github.com/repos/$owner/$name");
@ -173,23 +182,29 @@ class GitHub implements GitHost {
var response = await http.get(url, headers: headers);
if (response.statusCode != 200) {
Log.d("Github getRepo: Invalid response " +
Log.e("Github getRepo: Invalid response " +
response.statusCode.toString() +
": " +
response.body);
throw GitHostException.GetRepoFailed;
var ex = GitHostException.GetRepoFailed;
return Result.fail(ex);
}
Log.d("GitHub getRepo: " + response.body);
Map<String, dynamic> map = json.decode(response.body);
return repoFromJson(map);
try {
Map<String, dynamic> map = json.decode(response.body);
return Result(repoFromJson(map));
} on Exception catch (ex, st) {
return Result.fail(ex, st);
}
}
@override
Future addDeployKey(String sshPublicKey, String repo) async {
Future<Result<void>> addDeployKey(String sshPublicKey, String repo) async {
if (_accessCode.isEmpty) {
throw GitHostException.MissingAccessCode;
var ex = GitHostException.MissingAccessCode;
return Result.fail(ex);
}
var url = Uri.parse("https://api.github.com/repos/$repo/keys");
@ -212,11 +227,12 @@ class GitHub implements GitHost {
response.statusCode.toString() +
": " +
response.body);
throw GitHostException.DeployKeyFailed;
var ex = GitHostException.DeployKeyFailed;
return Result.fail(ex);
}
Log.d("GitHub addDeployKey: " + response.body);
return json.decode(response.body);
return Result(null);
}
static GitHostRepo repoFromJson(Map<String, dynamic> parsedJson) {
@ -261,9 +277,10 @@ class GitHub implements GitHost {
}
@override
Future<UserInfo?> getUserInfo() async {
Future<Result<UserInfo>> getUserInfo() async {
if (_accessCode.isEmpty) {
throw GitHostException.MissingAccessCode;
var ex = GitHostException.MissingAccessCode;
return Result.fail(ex);
}
var url = Uri.parse("https://api.github.com/user");
@ -278,7 +295,8 @@ class GitHub implements GitHost {
response.statusCode.toString() +
": " +
response.body);
return null;
var ex = GitHostException.HttpResponseFail;
return Result.fail(ex);
}
Map<String, dynamic>? map = jsonDecode(response.body);
@ -287,14 +305,16 @@ class GitHub implements GitHost {
response.statusCode.toString() +
": " +
response.body);
return null;
var ex = GitHostException.JsonDecodingFail;
return Result.fail(ex);
}
return UserInfo(
return Result(UserInfo(
name: map['name'],
email: map['email'],
username: map['login'],
);
));
}
String _buildAuthHeader() {

View File

@ -12,6 +12,8 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:gitjournal/utils/logger.dart';
import 'githost.dart';
// FIXME: Handle for edge cases of json.decode
class GitLab implements GitHost {
static const _clientID =
"faf33c3716faf05bfb701b1b31e36c83a23c3ec2d7161f4ff00fba2275524d09";
@ -67,9 +69,10 @@ class GitLab implements GitHost {
}
@override
Future<List<GitHostRepo>> listRepos() async {
Future<Result<List<GitHostRepo>>> listRepos() async {
if (_accessCode!.isEmpty) {
throw GitHostException.MissingAccessCode;
var ex = GitHostException.MissingAccessCode;
return Result.fail(ex);
}
// FIXME: pagination!
@ -82,11 +85,12 @@ class GitLab implements GitHost {
var response = await http.get(url);
if (response.statusCode != 200) {
Log.d("GitLab listRepos: Invalid response " +
Log.e("GitLab listRepos: Invalid response " +
response.statusCode.toString() +
": " +
response.body);
return [];
var ex = GitHostException.HttpResponseFail;
return Result.fail(ex);
}
List<dynamic> list = jsonDecode(response.body);
@ -98,13 +102,14 @@ class GitLab implements GitHost {
});
// FIXME: Sort these based on some criteria
return repos;
return Result(repos);
}
@override
Future<GitHostRepo> createRepo(String name) async {
Future<Result<GitHostRepo>> createRepo(String name) async {
if (_accessCode!.isEmpty) {
throw GitHostException.MissingAccessCode;
var ex = GitHostException.MissingAccessCode;
return Result.fail(ex);
}
var url = Uri.parse(
@ -121,58 +126,64 @@ class GitLab implements GitHost {
var response =
await http.post(url, headers: headers, body: json.encode(data));
if (response.statusCode != 201) {
Log.d("GitLab createRepo: Invalid response " +
Log.e("GitLab createRepo: Invalid response " +
response.statusCode.toString() +
": " +
response.body);
if (response.statusCode == 400) {
if (response.body.contains("has already been taken")) {
throw GitHostException.RepoExists;
var ex = GitHostException.RepoExists;
return Result.fail(ex);
}
}
throw GitHostException.CreateRepoFailed;
var ex = GitHostException.CreateRepoFailed;
return Result.fail(ex);
}
Log.d("GitLab createRepo: " + response.body);
Map<String, dynamic> map = json.decode(response.body);
return repoFromJson(map);
return Result(repoFromJson(map));
}
@override
Future<GitHostRepo> getRepo(String name) async {
Future<Result<GitHostRepo>> getRepo(String name) async {
if (_accessCode!.isEmpty) {
throw GitHostException.MissingAccessCode;
var ex = GitHostException.MissingAccessCode;
return Result.fail(ex);
}
var userInfo = await getUserInfo();
if (userInfo == null) {
throw Exception("GitLab no userInfo found!!");
var userInfoR = await getUserInfo();
if (userInfoR.isFailure) {
return fail(userInfoR);
}
var userInfo = userInfoR.getOrThrow();
var repo = userInfo.username + '%2F' + name;
var url = Uri.parse(
"https://gitlab.com/api/v4/projects/$repo?access_token=$_accessCode");
var response = await http.get(url);
if (response.statusCode != 200) {
Log.d("GitLab getRepo: Invalid response " +
Log.e("GitLab getRepo: Invalid response " +
response.statusCode.toString() +
": " +
response.body);
throw GitHostException.GetRepoFailed;
var ex = GitHostException.GetRepoFailed;
return Result.fail(ex);
}
Log.d("GitLab getRepo: " + response.body);
Map<String, dynamic> map = json.decode(response.body);
return repoFromJson(map);
return Result(repoFromJson(map));
}
@override
Future addDeployKey(String sshPublicKey, String repo) async {
Future<Result<void>> addDeployKey(String sshPublicKey, String repo) async {
if (_accessCode!.isEmpty) {
throw GitHostException.MissingAccessCode;
var ex = GitHostException.MissingAccessCode;
return Result.fail(ex);
}
repo = repo.replaceAll('/', '%2F');
@ -192,15 +203,16 @@ class GitLab implements GitHost {
var response =
await http.post(url, headers: headers, body: json.encode(data));
if (response.statusCode != 201) {
Log.d("GitLab addDeployKey: Invalid response " +
Log.e("GitLab addDeployKey: Invalid response " +
response.statusCode.toString() +
": " +
response.body);
throw GitHostException.DeployKeyFailed;
var ex = GitHostException.DeployKeyFailed;
return Result.fail(ex);
}
Log.d("GitLab addDeployKey: " + response.body);
return json.decode(response.body);
return Result(null);
}
static GitHostRepo repoFromJson(Map<String, dynamic> parsedJson) {
@ -245,9 +257,10 @@ class GitLab implements GitHost {
}
@override
Future<UserInfo?> getUserInfo() async {
Future<Result<UserInfo>> getUserInfo() async {
if (_accessCode!.isEmpty) {
throw GitHostException.MissingAccessCode;
var ex = GitHostException.MissingAccessCode;
return Result.fail(ex);
}
var url =
@ -255,27 +268,31 @@ class GitLab implements GitHost {
var response = await http.get(url);
if (response.statusCode != 200) {
Log.d("GitLab getUserInfo: Invalid response " +
Log.e("GitLab getUserInfo: Invalid response " +
response.statusCode.toString() +
": " +
response.body);
return null;
var ex = GitHostException.HttpResponseFail;
return Result.fail(ex);
}
Map<String, dynamic>? map = jsonDecode(response.body);
if (map == null || map.isEmpty) {
Log.d("GitLab getUserInfo: jsonDecode Failed " +
Log.e("GitLab getUserInfo: jsonDecode Failed " +
response.statusCode.toString() +
": " +
response.body);
return null;
var ex = GitHostException.JsonDecodingFail;
return Result.fail(ex);
}
return UserInfo(
return Result(UserInfo(
name: map['name'],
email: map['email'],
username: map['username'],
);
));
}
}

View File

@ -48,7 +48,7 @@ class OAuthAppState extends State<OAuthApp> {
child: const Text("List Repos"),
onPressed: () async {
try {
var repos = await githost!.listRepos();
var repos = await githost!.listRepos().getOrThrow();
for (var repo in repos) {
print(repo);
}

View File

@ -64,12 +64,12 @@ class GitHostSetupAutoConfigurePageState
_message = tr('setup.autoconfigure.readUser');
});
userInfo = await gitHost!.getUserInfo();
var userInfo = await gitHost!.getUserInfo().getOrThrow();
var settings = Provider.of<Settings>(context, listen: false);
if (userInfo != null && userInfo.name.isNotEmpty) {
if (userInfo.name.isNotEmpty) {
settings.gitAuthor = userInfo.name;
}
if (userInfo != null && userInfo.email.isNotEmpty) {
if (userInfo.email.isNotEmpty) {
settings.gitAuthorEmail = userInfo.email;
}
settings.save();

View File

@ -80,7 +80,7 @@ class GitHostSetupRepoSelectorState extends State<GitHostSetupRepoSelector> {
Log.d("Starting RepoSelector");
try {
var allRepos = await widget.gitHost.listRepos();
var allRepos = await widget.gitHost.listRepos().getOrThrow();
allRepos.sort((GitHostRepo a, GitHostRepo b) {
if (a.updatedAt != null && b.updatedAt != null) {
return a.updatedAt!.compareTo(b.updatedAt!);
@ -210,7 +210,8 @@ class GitHostSetupRepoSelectorState extends State<GitHostSetupRepoSelector> {
try {
var repoName = _textController.text.trim();
var repo = await widget.gitHost.createRepo(repoName);
var repo =
await widget.gitHost.createRepo(repoName).getOrThrow();
widget.onDone(repo);
return;
} catch (e, stacktrace) {