-
Notifications
You must be signed in to change notification settings - Fork 308
Subscription api events #382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@gnprice This is ready for comments! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Comments below.
One high-level comment that runs through several of those below: the second commit
130117a model: Refactor Subscription to extend from ZulipStream
combines a change that touches a number of lines of code, almost all of them in a boring way (the refactor to make Subscription extend ZulipStream) with several changes that affect a smaller number of lines of code and in a much more substantive way (in particular, taking several updates to the Zulip server API).
In general one important aspect of clear and coherent commits is that those two kinds of changes should happen in separate commits. The two call for quite different approaches to reading — one of them for rapidly scanning to confirm that some straightforward pattern is reliably followed, and to spot the few lines that aren't part of that pattern, while the other kind of change calls for closer investigation and thought. Keeping them separate therefore helps make both kinds of change easier to read and review.
lib/api/model/model.dart
Outdated
admin(apiValue: 2), | ||
member(apiValue: 3), | ||
moderator(apiValue: 4), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
admin(apiValue: 2), | |
member(apiValue: 3), | |
moderator(apiValue: 4), | |
administrators(apiValue: 2), | |
fullMembers(apiValue: 3), | |
moderators(apiValue: 4), |
The substantive bit here is that value 3 is "full members"; if you follow the link on that term in the doc, you'll find the definition, which is a smaller set than "members".
Then in general Zulip's style is to avoid abbreviations. … I'm surprised to find this doesn't seem to appear in the zulip.readthedocs.io docs, but the Flutter style guide has a good phrasing of a similar policy:
Unless the abbreviation is more recognizable than the expansion (e.g. XML, HTTP, JSON), expand abbrevations when selecting a name for an identifier.
Hence "administrator" rather than "admin". In general I think I take less of a hard line on this point than Tim does, but here there's really no cost to spelling it out.
And the singular could work, but: given the migration to groups for permissions, which has now begun to get underway, all policies like this one should be thought of as just a constrained way of expressing a group (one where you get to choose from a fixed finite list of possible groups). And the groups are naturally named as the plural of their members. So, "administrators" and so on.
lib/api/model/model.dart
Outdated
static int _readCanRemoveSubscribersGroup(Map json, String key) { | ||
return json[key] ?? json['can_remove_subscribers_group_id']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should return int?
, right?
Relatedly: this should have tests 🙂. I believe this version would throw when neither key is present (so, for servers before FL 142), and a test would catch that.
final int? canRemoveSubscribersGroupId; // TODO(server-6) | ||
// TODO(server-6): `canRemoveSubscribersGroupId` added in FL 142 | ||
// TODO(server-8): in FL 197 renamed to `canRemoveSubscribersGroup` | ||
@JsonKey(readValue: _readCanRemoveSubscribersGroup) | ||
final int? canRemoveSubscribersGroup; | ||
|
||
// TODO(server-8): added in FL 199, was previously only on [Subscription] objects | ||
final int? streamWeeklyTraffic; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's put these changes (which reflect updates to the API) in a separate commit coming before the commit that reworks the Subscription
class.
final int? streamWeeklyTraffic; | ||
|
||
final bool inviteOnly; | ||
final bool? isWebPublic; // TODO(server-??): doc doesn't say when added |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a functional change, even though a small one: this field becomes non-nullable, so we start assuming it must be present when deserializing a Subscription
. Let's therefore put it in a separate commit from the "Subscription extends ZulipStream" refactor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also
changed `isWebPublic` to be non-null. It has been a
field on subscriptions since at least FL 8 (see
zulip/zulip@f93c19ec6 in `zerver/openapi/zulip.yaml`
and `gather_subscriptions_helper` in `zerver/lib/actions.py`).
Thanks. I don't understand the last bit, though — what specifically should the reader be looking at in that function? (It's a long function; and searching there for is_web_public
doesn't find anything.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was a bit of a journey tracking it down, the openapi docs shows the existence of is_web_public
on subscriptions but on the /users/me/subscriptions
endpoint. To confirm it's in the data bundle from register
the journey went from zerver.views.events_register.events_register_backend
-> zerver.lib.events.do_events.register
-> zerver.lib.events.fetch_initial_state_data
and from there tracking down what populates subscriptions
. I've amended the comment with more context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On further exploring, the docs on Subscription
model specifically mentions that it merges in Stream.API_FIELDS
for clients so that'll be a clearer place to reference.
lib/api/model/model.dart
Outdated
/// in <https://zulip.com/api/register-queue>. | ||
@JsonSerializable(fieldRename: FieldRename.snake) | ||
class Subscription extends ZulipStream { | ||
List<int>? subscribers; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- // final List<int> subscribers; // we register with includeSubscribers false
+ List<int>? subscribers;
Why this change? Seems clearer to leave this field out, given that it'll never be present; but in any case if we were going to add it, that change should come in a commit that isn't mixed in with the big refactor to this class.
lib/api/model/events.dart
Outdated
@JsonKey(includeToJson: true) | ||
String get op; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this @JsonKey
line have any effect?
I suspect it doesn't — I believe the overrides effectively clobber whatever annotations were or weren't present on the base class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't, removed this instance of it
lib/model/store.dart
Outdated
} else if (event is SubscriptionUpdateEvent) { | ||
assert(debugLog("server event: subscription/update")); | ||
final subscription = subscriptions[event.streamId]; | ||
if (subscription == null) return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (subscription == null) return; | |
if (subscription == null) return; // TODO(log) |
This situation shouldn't be possible, so we should log when it happens (once we set up logging at all).
lib/model/store.dart
Outdated
case SubscriptionProperty.unknown: | ||
// unrecognized setting; do nothing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: names in comments should match those in code:
case SubscriptionProperty.unknown: | |
// unrecognized setting; do nothing | |
case SubscriptionProperty.unknown: | |
// unrecognized property; do nothing |
test/example_data.dart
Outdated
pinToTop: pinToTop ?? false, | ||
isMuted: isMuted ?? false, | ||
color: color ?? "#FF0000", | ||
emailAddress: emailAddress ?? "", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The empty string doesn't look like a very plausible email address. Here's a better version:
emailAddress: emailAddress ?? "", | |
emailAddress: emailAddress ?? "[email protected]", |
In general the example data in this file should have the right basic shape for real data, at least to the extent that's easy to do.
test/example_data.dart
Outdated
/// | ||
/// We only allow overrides of values specific to the [Subscription], all | ||
/// other properties are copied from the [ZulipStream] provided. | ||
Subscription streamSubscription( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can name this more simply:
Subscription streamSubscription( | |
Subscription subscription( |
because I don't think we'll need an alternate way of making an example subscription.
1b8b9f6
to
cb251c0
Compare
@gnprice this is ready for review again! |
lib/api/model/model.dart
Outdated
// final List<int> subscribers; // we register with includeSubscribers false | ||
|
||
final int streamPostPolicy; // TODO enum | ||
// final bool? isAnnouncementOnly; // deprecated; ignore | ||
final String emailAddress; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove `emailAddress` field per update in FL 266. Also
FL 226
final int? streamWeeklyTraffic; | ||
|
||
final bool inviteOnly; | ||
final bool? isWebPublic; // TODO(server-??): doc doesn't say when added |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also
changed `isWebPublic` to be non-null. It has been a
field on subscriptions since at least FL 8 (see
zulip/zulip@f93c19ec6 in `zerver/openapi/zulip.yaml`
and `gather_subscriptions_helper` in `zerver/lib/actions.py`).
Thanks. I don't understand the last bit, though — what specifically should the reader be looking at in that function? (It's a long function; and searching there for is_web_public
doesn't find anything.)
final bool? isWebPublic; // TODO(server-??): doc doesn't say when added | ||
final bool historyPublicToSubscribers; | ||
final int? messageRetentionDays; | ||
class Subscription extends ZulipStream { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
api: Change Subscription to extend from ZulipStream
This can now say api [nfc]:
, which is nice.
|
||
final int? canRemoveSubscribersGroupId; // TODO(server-6) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bump
lib/api/model/model.dart
Outdated
// final bool? inHomeView; // deprecated; ignore | ||
|
||
final String color; | ||
String color; | ||
String emailAddress; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I wouldn't describe the fields as grouped by data type — in main they look like this:
// First, fields that are about the stream and not the user's relation to it.
// These are largely the same as in [ZulipStream].
// …
// final List<int> subscribers; // we register with includeSubscribers false
final int streamPostPolicy; // TODO enum
// final bool? isAnnouncementOnly; // deprecated; ignore
final String emailAddress;
final int? canRemoveSubscribersGroupId; // TODO(server-6)
// Then, fields that are specific to the subscription,
// i.e. the user's relationship to the stream.
So emailAddress
was in amidst subscribers
and streamPostPolicy
and others because they're fields that are about the stream and not the user's relation to it. If we were keeping it, I'd probably want it still next to subscribers
for the same reason.
Moot because we're deleting it, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the revision! Generally this looks good now — various small comments above and below.
final bool pinToTop; | ||
|
||
final bool isMuted; | ||
bool pinToTop; | ||
bool isMuted; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this blank line gets deleted in the otherwise-unrelated final commit
I think it's fine with or without the blank line — it's true that pinToTop
and isMuted
are conceptually related — but the change should happen in a commit that's already touching that code. Probably
488de5f api: Change Subscription to extend from ZulipStream
is the most relevant.
lib/api/model/model.dart
Outdated
/// For docs, search for "stream_post_policy" | ||
/// in <https://zulip.com/api/get-stream-by-id> | ||
@JsonEnum(valueField: 'apiValue') | ||
enum StreamPostPolicy{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
enum StreamPostPolicy{ | |
enum StreamPostPolicy { |
final int streamPostPolicy; // TODO enum | ||
// final bool isAnnouncementOnly; // deprecated; ignore | ||
final StreamPostPolicy streamPostPolicy; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: s/JsonEnum/enum/ in commit message:
api: Change streamPostPolicy to JsonEnum
I wouldn't say this field is now "a JsonEnum" — rather, it's an enum, specifically a Dart enum. And that enum type is annotated with JsonEnum
… which isn't so much a name of a kind of enum, but rather is an annotation saying how to relate some enum to JSON.
required this.canRemoveSubscribersGroupId, | ||
required this.canRemoveSubscribersGroup, | ||
required this.streamWeeklyTraffic, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
api: Update ZulipStream for api changes
"for API changes" — in English as opposed to code, "API" is an initialism written in all caps.
cb251c0
to
3ffb88b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @sirpengi for the revision! This now all looks good except there's one piece of the commit structure that's still a bit messy. Let's handle it by just adding to a couple of the commit messages.
Unpacking #382 (comment) and #382 (comment) : as long as the commit that renames canRemoveSubscribersGroupId
to canRemoveSubscribersGroup
happens at a point where both Stream
and Subscription
separately define that field, the same commit should handle it for both of them.
That's the cleanest way to arrange things for reading and understanding the commits, because the changes in those two parts of the code are basically two manifestations of the same logical change. So when a commit has just one half of that, it looks incomplete.
In order to not spend too much time polishing the branch from here, let's handle this this time by just explaining in the commit message.
Specifically in the commit that does the first part of that change:
015130a api: Update ZulipStream for API changes
the commit message can just explain that the other half of the canRemoveSubscribersGroup
rename, for Subscription
, will happen in the next commit. And then the commit message for that next commit:
916319b api: Update Subscription for API changes
can say it's completing that rename.
That then puts the reader on the same page as the author when they're looking at each commit: they know the author is aware it's only half of the change, and they know where to look for the other half.
For future API updates, probably the best general branch structure is to have a separate commit per logical API change — basically a separate commit for each separate feature level at which changes happened. So e.g. here there'd be
- a commit for making
Subscription.isWebPublic
non-nullable, complete with the commit message explaining why that's justified - a commit for the FL 197 rename of
canRemoveSubscribersGroupId
tocanRemoveSubscribersGroup
, on both streams and subscriptions - a commit for the FL 199 addition of
ZulipStream.streamWeeklyTraffic
- a commit for the FL 226 removal of
Subscription.emailAddress
in place of the two commits
015130a api: Update ZulipStream for API changes
916319b api: Update Subscription for API changes
.
The main exception to the "one commit per feature level" pattern would be if several changes that are logically closely related are spread across several feature levels. That occasionally happens, typically when a complex product feature is being added and gets merged as several server PRs over time.
3ffb88b
to
0d149fe
Compare
Add `streamWeeklyTraffic` and handle rename of `canRemoveSubscribersGroupId`. This rename also affects `Subscription` but will happen in the next commit.
0d149fe
to
e58cf25
Compare
Complete rename of `canRemoveSubscribersGroupId` that was done in `ZulipStream` in previous commit. Also change `isWebPublic` to be non-null. It has been a field on subscriptions since at least FL 8 (see zulip/zulip@f93c19ec6 in `zerver/openapi/zulip.yaml` and the docs attached to `Subscription.API_FIELDS` in `zerver/models.py`).
Add events for subscription with `op` values of `add`, `remove`, and `update`. `peer_add` and `peer_remove` left for zulip#374.
e58cf25
to
3686ca2
Compare
@gnprice comments handled. And understood about the concept of breaking api updates like this into discrete chunks. I was initially adverse to layering changes across commits that touch the same bits multiple times (and in the case of |
Thanks! Looks good; merging.
Yeah. For that reason probably the ideal shape for this particular series would be (as mentioned at #382 (comment)) to have the commit
Sounds good! |
Clean up
Subscription
andZulipStream
: makeSubscription
extendZulipStream
and move fields around so onlySubscription
specific fields are in the class definition. Also handles the eventssubscription/op:add
,subscription/op:remove
, andsubscription/op:update
.Fixes part of #187