Skip to content

Commit

Permalink
Various additions to create RedditBase objects and Stream<RedditBase>…
Browse files Browse the repository at this point in the history
… generators (#6)

-Added code to turn Reddit API responses into RedditBase objects.
-Added an abstract class, ListingGenerator, which contains a static
method which allows for asynchronous iteration over Reddit API listing
results.
-Implemented a subset of the User class.
-Created stubs for Redditor and Subreddit classes.
-Updated documentation for various classes.
  • Loading branch information
bkonyi authored Aug 4, 2017
1 parent dff27f2 commit 1f2b8a7
Show file tree
Hide file tree
Showing 12 changed files with 505 additions and 16 deletions.
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ dart:
dart_task:
- test: -p vm
- dartfmt
- dartanalyzer --strong --fatal-warnings --fatal-lints

script:
- dartanalyzer --strong --fatal-warnings --fatal-lints .
166 changes: 166 additions & 0 deletions lib/src/api_paths.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright (c) 2017, the Dart Reddit API Wrapper project authors.
// Please see the AUTHORS file for details. All rights reserved.
// Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

/// A [Map] containing all of the Reddit API paths.
final Map apiPath = {
'about_edited': 'r/{subreddit}/about/edited/',
'about_log': 'r/{subreddit}/about/log/',
'about_modqueue': 'r/{subreddit}/about/modqueue/',
'about_reports': 'r/{subreddit}/about/reports/',
'about_spam': 'r/{subreddit}/about/spam/',
'about_sticky': 'r/{subreddit}/about/sticky/',
'about_stylesheet': 'r/{subreddit}/about/stylesheet/',
'about_traffic': 'r/{subreddit}/about/traffic/',
'about_unmoderated': 'r/{subreddit}/about/unmoderated/',
'accept_mod_invite': 'r/{subreddit}/api/accept_moderator_invite',
'approve': 'api/approve/',
'block': 'api/block',
'blocked': 'prefs/blocked/',
'comment': 'api/comment/',
'comment_replies': 'message/comments/',
'compose': 'api/compose/',
'contest_mode': 'api/set_contest_mode/',
'del': 'api/del/',
'deleteflair': 'r/{subreddit}/api/deleteflair',
'delete_sr_banner': 'r/{subreddit}/api/delete_sr_banner',
'delete_sr_header': 'r/{subreddit}/api/delete_sr_header',
'delete_sr_icon': 'r/{subreddit}/api/delete_sr_icon',
'delete_sr_image': 'r/{subreddit}/api/delete_sr_img',
'distinguish': 'api/distinguish/',
'domain': 'domain/{domain}/',
'duplicates': 'duplicates/{submission_id}/',
'edit': 'api/editusertext/',
'flair': 'r/{subreddit}/api/flair/',
'flairconfig': 'r/{subreddit}/api/flairconfig/',
'flaircsv': 'r/{subreddit}/api/flaircsv/',
'flairlist': 'r/{subreddit}/api/flairlist/',
'flairselector': 'r/{subreddit}/api/flairselector/',
'flairtemplate': 'r/{subreddit}/api/flairtemplate/',
'flairtemplateclear': 'r/{subreddit}/api/clearflairtemplates/',
'flairtemplatedelete': 'r/{subreddit}/api/deleteflairtemplate/',
'friend': 'r/{subreddit}/api/friend/',
'friend_v1': 'api/v1/me/friends/{user}',
'friends': 'api/v1/me/friends/',
'gild_thing': 'api/v1/gold/gild/{fullname}/',
'gild_user': 'api/v1/gold/give/{username}/',
'hide': 'api/hide/',
'ignore_reports': 'api/ignore_reports/',
'inbox': 'message/inbox/',
'info': 'api/info/',
'karma': 'api/v1/me/karma',
'leavecontributor': 'api/leavecontributor',
'leavemoderator': 'api/leavemoderator',
'list_banned': 'r/{subreddit}/about/banned/',
'list_contributor': 'r/{subreddit}/about/contributors/',
'list_moderator': 'r/{subreddit}/about/moderators/',
'list_muted': 'r/{subreddit}/about/muted/',
'list_wikibanned': 'r/{subreddit}/about/wikibanned/',
'list_wikicontributor': 'r/{subreddit}/about/wikicontributors/',
'live_accept_invite': 'api/live/{id}/accept_contributor_invite',
'live_add_update': 'api/live/{id}/update',
'live_close': 'api/live/{id}/close_thread',
'live_contributors': 'live/{id}/contributors',
'live_discussions': 'live/{id}/discussions',
'live_info': 'api/live/by_id/{ids}',
'live_invite': 'api/live/{id}/invite_contributor',
'live_leave': 'api/live/{id}/leave_contributor',
'live_now': 'api/live/happening_now',
'live_remove_update': 'api/live/{id}/delete_update',
'live_remove_contrib': 'api/live/{id}/rm_contributor',
'live_remove_invite': 'api/live/{id}/rm_contributor_invite',
'live_report': 'api/live/{id}/report',
'live_strike': 'api/live/{id}/strike_update',
'live_update_perms': 'api/live/{id}/set_contributor_permissions',
'live_update_thread': 'api/live/{id}/edit',
'live_updates': 'live/{id}',
'liveabout': 'api/live/{id}/about/',
'livecreate': 'api/live/create',
'lock': 'api/lock/',
'me': 'api/v1/me',
'mentions': 'message/mentions',
'message': 'message/messages/{id}/',
'messages': 'message/messages/',
'moderator_messages': 'r/{subreddit}/message/moderator/',
'moderator_unread': 'r/{subreddit}/message/moderator/unread/',
'morechildren': 'api/morechildren/',
'my_contributor': 'subreddits/mine/contributor/',
'my_moderator': 'subreddits/mine/moderator/',
'my_multireddits': 'api/multi/mine/',
'my_subreddits': 'subreddits/mine/subscriber/',
'marknsfw': 'api/marknsfw/',
'modmail_archive': 'api/mod/conversations/{id}/archive',
'modmail_conversation': 'api/mod/conversations/{id}',
'modmail_conversations': 'api/mod/conversations/',
'modmail_highlight': 'api/mod/conversations/{id}/highlight',
'modmail_mute': 'api/mod/conversations/{id}/mute',
'modmail_read': 'api/mod/conversations/read',
'modmail_unarchive': 'api/mod/conversations/{id}/unarchive',
'modmail_unmute': 'api/mod/conversations/{id}/unmute',
'modmail_unread': 'api/mod/conversations/unread',
'multireddit': 'user/{user}/m/{multi}/',
'multireddit_api': 'api/multi/user/{user}/m/{multi}/',
'multireddit_base': 'api/multi/',
'multireddit_copy': 'api/multi/copy/',
'multireddit_rename': 'api/multi/rename/',
'multireddit_update': 'api/multi/user/{user}/m/{multi}/r/{subreddit}',
'multireddit_user': 'api/multi/user/{user}/',
'mute_sender': 'api/mute_message_author/',
'quarantine_opt_in': 'api/quarantine_optin',
'quarantine_opt_out': 'api/quarantine_optout',
'read_message': 'api/read_message/',
'remove': 'api/remove/',
'report': 'api/report/',
'rules': 'r/{subreddit}/about/rules',
'save': 'api/save/',
'search': 'r/{subreddit}/search/',
'select_flair': 'r/{subreddit}/api/selectflair/',
'sent': 'message/sent/',
'setpermissions': 'r/{subreddit}/api/setpermissions/',
'spoiler': 'api/spoiler/',
'site_admin': 'api/site_admin/',
'sticky_submission': 'api/set_subreddit_sticky/',
'sub_recommended': 'api/recommend/sr/{subreddits}',
'submission': 'comments/{id}/',
'submission_replies': 'message/selfreply/',
'submit': 'api/submit/',
'subreddit': 'r/{subreddit}/',
'subreddit_about': 'r/{subreddit}/about/',
'subreddit_filter': ('api/filter/user/{user}/f/{special}/'
'r/{subreddit}'),
'subreddit_filter_list': 'api/filter/user/{user}/f/{special}',
'subreddit_random': 'r/{subreddit}/random/',
'subreddit_settings': 'r/{subreddit}/about/edit/',
'subreddit_stylesheet': 'r/{subreddit}/api/subreddit_stylesheet/',
'subreddits_by_topic': 'api/subreddits_by_topic',
'subreddits_default': 'subreddits/default/',
'subreddits_gold': 'subreddits/gold/',
'subreddits_new': 'subreddits/new/',
'subreddits_popular': 'subreddits/popular/',
'subreddits_name_search': 'api/search_reddit_names/',
'subreddits_search': 'subreddits/search/',
'subscribe': 'api/subscribe/',
'suggested_sort': 'api/set_suggested_sort/',
'unfriend': 'r/{subreddit}/api/unfriend/',
'unhide': 'api/unhide/',
'unignore_reports': 'api/unignore_reports/',
'unlock': 'api/unlock/',
'unmarknsfw': 'api/unmarknsfw/',
'unmute_sender': 'api/unmute_message_author/',
'unread': 'message/unread/',
'unread_message': 'api/unread_message/',
'unsave': 'api/unsave/',
'unspoiler': 'api/unspoiler/',
'upload_image': 'r/{subreddit}/api/upload_sr_img',
'user': 'user/{user}/',
'user_about': 'user/{user}/about/',
'vote': 'api/vote/',
'wiki_edit': 'r/{subreddit}/api/wiki/edit/',
'wiki_page': 'r/{subreddit}/wiki/{page}',
'wiki_page_editor': 'r/{subreddit}/api/wiki/alloweditor/{method}',
'wiki_page_revisions': 'r/{subreddit}/wiki/revisions/{page}',
'wiki_page_settings': 'r/{subreddit}/wiki/settings/{page}',
'wiki_pages': 'r/{subreddit}/wiki/pages/',
'wiki_revisions': 'r/{subreddit}/wiki/revisions/'
};
12 changes: 6 additions & 6 deletions lib/src/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,14 @@ abstract class Authenticator {

/// Make a simple `GET` request. [path] is the destination URI that the
/// request will be made to.
Future<Map> get(Uri path) async {
return _request(kGetRequest, path);
Future<Map> get(Uri path, {Map params}) async {
return _request(kGetRequest, path, params: params);
}

/// Make a simple `POST` request. [path] is the destination URI and [body]
/// contains the POST parameters that will be sent with the request.
Future<Map> post(Uri path, Map<String, String> body) async {
return _request(kPostRequest, path, body);
return _request(kPostRequest, path, body: body);
}

/// Request data from Reddit using our OAuth2 client.
Expand All @@ -127,15 +127,16 @@ abstract class Authenticator {
/// request parameters. [body] is an optional parameter which contains the
/// body fields for a POST request.
Future<Map> _request(String type, Uri path,
[Map<String, String> body]) async {
{Map<String, String> body, Map params}) async {
if (_client == null) {
throw new DRAWAuthenticationError(
'The authenticator does not have a valid token.');
}
if (!isValid) {
await refresh();
}
final request = new http.Request(type, path);
final finalPath = path.replace(queryParameters: params);
final request = new http.Request(type, finalPath);
if (body != null) {
request.bodyFields = body;
}
Expand All @@ -161,7 +162,6 @@ abstract class Authenticator {

final httpClient = new http.Client();
final start = new DateTime.now();

final headers = new Map<String, String>();
headers[kUserAgentKey] = _userAgent;

Expand Down
44 changes: 44 additions & 0 deletions lib/src/base.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2017, the Dart Reddit API Wrapper project authors.
// Please see the AUTHORS file for details. All rights reserved.
// Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

import 'reddit.dart';

class RedditBase {
final Reddit reddit;
final Map _data;
final RegExp _snakecaseRegexp = new RegExp("[A-Z]");

RedditBase(this.reddit) : _data = null;

RedditBase.loadData(this.reddit, Map data) : _data = data;

String _snakeCase(String name, [separator = '_']) => name.replaceAllMapped(
_snakecaseRegexp,
(Match match) =>
(match.start != 0 ? separator : '') + match.group(0).toLowerCase());

dynamic noSuchMethod(Invocation invocation) {
// This is a dirty hack to allow for dynamic field population based on the
// API response instead of hardcoding these fields and having to update them
// when the API updates. Invocation.memberName is a Symbol, which
// unfortunately doesn't have a getName method due to code minification
// restrictions in dart2js, so the only way to get the name properly is
// using the dart:mirrors library. Unfortunately, dart:mirrors isn't
// supported in Flutter/Dart AOT, which makes it unacceptable to use in this
// library. However, Symbol.toString() returns a string in the form of
// Symbol("memberName") consistently on the Dart VM. We're abusing this
// behaviour here, and there's no promise that this will work in the future,
// but there's no reason to assume that this behaviour will change any time
// soon.
var name = invocation.memberName.toString();
name = _snakeCase(name.substring(8, name.length - 2));
if (!invocation.isGetter || (_data == null) || !_data.containsKey(name)) {
// Check that the accessed field is a getter and the property exists.
throw new NoSuchMethodError(this, invocation.memberName,
invocation.positionalArguments, invocation.namedArguments);
}
return _data[name];
}
}
19 changes: 17 additions & 2 deletions lib/src/exceptions.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
// Copyright (c) 2017, the Dart Reddit API Wrapper project authors.
// Copyright (c) 2017, the Dart Reddit API Wrapper project authors.
// Please see the AUTHORS file for details. All rights reserved.
// Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

// TODO(bkonyi) document properly.
/// Thrown when there is an error during the authentication flow.
class DRAWAuthenticationError implements Exception {
DRAWAuthenticationError(this.message);
String message;
String toString() => 'DRAWAuthenticationError: $message';
}

/// Thrown by unfinished code that hasn't yet implemented all the features it
/// needs.
class DRAWUnimplementedError extends UnimplementedError {
DRAWUnimplementedError([String message]) : super(message);
}

/// Thrown due to a fatal error encountered inside DRAW. If you're not adding
/// functionality to DRAW you should never see this. Otherwise, please file a
/// bug at github.com/draw-dev/DRAW/issues.
class DRAWInternalError implements Exception {
DRAWInternalError(this.message);
String message;
String toString() => 'DRAWInternalError: $message';
}
57 changes: 57 additions & 0 deletions lib/src/listing/listing_generator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2017, the Dart Reddit API Wrapper project authors.
// Please see the AUTHORS file for details. All rights reserved.
// Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

import 'dart:async';

import '../reddit.dart';

/// An abstract static class used to generate [Stream]s of [RedditBase] objects.
/// This class should not be used directly, as it is used by various methods
/// defined in children of [RedditBase].
abstract class ListingGenerator {
static const defaultRequestLimit = 100;

/// An asynchronous iterator method used to make Reddit API calls as defined
/// by [api] in blocks of size [limit]. The default [limit] is specified by
/// [defaultRequestLimit]. Returns a [Stream<T>] which can be iterated over
/// using an asynchronous for-loop.
static Stream<T> generator<T>(final Reddit reddit, final String api,
{int limit, Map params}) async* {
final kLimitKey = 'limit';
final nullLimit = 1024;
final paramsInternal = params ?? new Map();
paramsInternal[kLimitKey] = (limit ?? nullLimit).toString();
int yielded = 0;
int index = 0;
List<T> listing;

Future<List<T>> _nextBatch() async {
final response = (await reddit.get(api, params: paramsInternal)) as Map;
final newListing = response['listing'];
paramsInternal['after'] = response['after'];
if (newListing.length == 0) {
return null;
}
index = 0;
return newListing;
}

while (yielded < limit) {
if ((listing == null) || (index >= listing.length)) {
if (listing != null &&
listing.length < int.parse(paramsInternal[kLimitKey])) {
break;
}
listing = await _nextBatch();
if (listing == null) {
break;
}
}
yield listing[index];
++index;
++yielded;
}
}
}
21 changes: 21 additions & 0 deletions lib/src/models/redditor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) 2017, the Dart Reddit API Wrapper project authors.
// Please see the AUTHORS file for details. All rights reserved.
// Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

import '../base.dart';
import '../exceptions.dart';
import '../reddit.dart';

class Redditor extends RedditBase {
String _name;
Uri _path;

Redditor.parse(Reddit reddit, Map data) : super.loadData(reddit, data) {
if (!data.containsKey('name')) {
// TODO(bkonyi) throw invalid object exception
throw new DRAWUnimplementedError();
}
_name = data['name'];
}
}
13 changes: 13 additions & 0 deletions lib/src/models/subreddit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2017, the Dart Reddit API Wrapper project authors.
// Please see the AUTHORS file for details. All rights reserved.
// Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

import '../base.dart';
import '../reddit.dart';

// TODO(bkonyi) implement
class Subreddit extends RedditBase {
Subreddit.parse(Reddit reddit, Map data)
: super.loadData(reddit, data['data']);
}
Loading

0 comments on commit 1f2b8a7

Please sign in to comment.