Skip to content

Instantly share code, notes, and snippets.

@kaspth

kaspth/events.rb Secret

Created August 6, 2021 16:39
Show Gist options
  • Save kaspth/c6797aa3a913d989215c66350235589d to your computer and use it in GitHub Desktop.
Save kaspth/c6797aa3a913d989215c66350235589d to your computer and use it in GitHub Desktop.
Some domain modeling done for @tbcooney and @openunit
module User::EventSubscriber
extend ActiveSupport::Concern
included do
has_many :event_subscriptions, dependent: :destroy
end
# Current.user.subscribe_to_events_from(Current.facility, **facility_event_params)
# Current.user.subscribe_to_events_from(Current.account, **account_event_params)
# ^ though I can't remember if params can be used with ** yet, may need explicit casting via to_h.
def subscribe_to_events_from(source, active: true, **events)
subscription_for(source).update! active: active, events: events
end
# Current.user.unsubscribe_from(Current.account)
def unsubscribe_from(source)
subscription_for(source).inactive!
end
# @user.subscription_for(facility)
def subscription_for(source)
event_subscriptions.find_or_initialize_by(source: source)
end
end
class Event::Subscription < ApplicationRecord
belongs_to :user
belongs_to :account, default: -> { source&.account } # I'd add a def account; self; end method to Account to ease this kind of duck typing
belongs_to :source, polymorphic: true
# Adds convenience accessor for one off checks e.g. subscription.new_reservation?
store :events, accessors: Events.all_events, coder: ActiveSupport::JSON
after_initialize :scope_to_known_source_events
# Adds convenience methods using Ruby 3.0 shorthand method syntax. Grouped and condensed to highlight their similarity and the pattern.
def active! = update! active: true
def inactive! = update! active: false
def broadcast_for?(event)
active? && enabled_for?(event)
end
def enabled_for?(event)
try(event)
end
private
# Means that it's safe to do @user.subscription_for(facility).events.each do |event| in the view
def scope_to_known_source_events
events.slice!(*Events.events_for(self))
end
end
class Event::Subscription::Events
GENERAL_EVENTS = %w[ first second third ].freeze
# Should be able to add events to ^ or these arrays and have them pop up in the view as a valid new event to subscribe to.
TYPE_SPECIFIC_EVENTS = {
"Account" => %w[ first second ].freeze,
"Facility" => %w[ first second ].freeze
}
mattr_reader :all_events, default: GENERAL_EVENTS + TYPE_SPECIFIC_EVENTS.values.flatten.uniq
def self.events_for(subscription)
GENERAL_EVENTS + TYPE_SPECIFIC_EVENTS.fetch(subscription.source_type) # Use fetch to raise and prevent usage with unknown types
end
end
class SomeMailer < ApplicationMailer
after_action :prevent_delivery_for_disabled_events
def some_mail
@event = :first
mail subject: "Yo yo"
end
private
def prevent_delivery_for_disabled_events
mail.perform_deliveries = params[:recipient].subscription_for(@facility).broadcast_for?(@event)
end
end
# I like the Feature class name! (And I dislike when conditional Features are called flags 😂)
# Here I've picked Rollout as the domain language to emphasize the relatively short lived nature of this preliminary feature access — and that there's an end in sight.
module ConditionalRollout
extend ActiveSupport::Concern
class_methods do
# Using before_action means you can attach the standard options too and gain flexibility for next to no cost.
# requires_preliminary_access_to :some_feature, only: :index, if: -> { Current.user.übercool? }
def requires_preliminary_access_to(feature_name, **options)
# I'd hoist the admin? conditional since it doesn't quite fit with the account level details.
# This also means a `admin_overrule: false` option could more easily be added to this method if need be.
before_action -> { enforce_access_to feature_name unless Current.user&.admin? }, **options
end
end
private
def enforce_access_to(feature_name)
unless current_account.has_preliminary_access_to?(feature)
raise AbstractController::ActionNotFound
end
end
end
# app/models/account/rollout.rb
# included in Account
# No need to extend ActiveSupport::Concern since we're just adding some constants and an instance method.
module Account::Rollout
BETA_ACCOUNTS = %w(some-domain)
FEATURES = {
some_feature: []
}
def has_preliminary_access_to?(feature)
if domains = FEATURES.dig(feature.to_sym)
domain.in?(BETA_ACCOUNTS) || domain.in?(domains)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment