Module: PlanMyStuff::Notifications

Defined in:
lib/plan_my_stuff/notifications.rb

Overview

Central instrumentation helper. Domain classes call PlanMyStuff::Notifications.instrument at mutation points so consuming apps can subscribe for email, webhooks, Slack, etc.

Events are fired under the <event>.plan_my_stuff namespace via ActiveSupport::Notifications (Rails convention: event first, library last - matches sql.active_record, deliver.action_mailer). Subscribers run synchronously.

Constant Summary collapse

EVENT_SUFFIX =
'plan_my_stuff'
SKIPPED_LOG_KEYS =
%i[user timestamp visibility visibility_allowlist].freeze

Class Method Summary collapse

Class Method Details

.build_payload(resource, actor, extra) ⇒ Hash

Builds the payload hash for an event.

Parameters:

  • resource (Object)
  • actor (Object, nil)
  • extra (Hash)

Returns:

  • (Hash)


57
58
59
60
61
62
63
64
65
# File 'lib/plan_my_stuff/notifications.rb', line 57

def build_payload(resource, actor, extra)
  payload = {
    infer_resource_key(resource) => resource,
    :user => actor,
    :timestamp => Time.current,
  }
  payload.merge!(visibility_fields(resource))
  payload.merge(extra)
end

.infer_resource_key(resource) ⇒ Symbol

Maps a resource object to its payload key. An Array recurses on its first element and pluralizes that key, so batch events carry the full set under one key (a batch of project items keys as :project_items, a batch of issues as :issues, an empty/unknown batch as :resources).

Parameters:

  • resource (Object)

Returns:

  • (Symbol)


75
76
77
78
79
80
81
82
83
# File 'lib/plan_my_stuff/notifications.rb', line 75

def infer_resource_key(resource)
  case resource
  when PlanMyStuff::Issue then :issue
  when PlanMyStuff::Comment then :comment
  when PlanMyStuff::BaseProjectItem then :project_item
  when Array then :"#{infer_resource_key(resource.first)}s"
  else :resource
  end
end

.instrument(event, resource, user: nil, **extra) ⇒ void

This method returns an undefined value.

Fires <event>.plan_my_stuff with a normalized payload.

Parameters:

  • event (String)

    e.g. ‘issue_created’

  • resource (Object)

    domain object (Issue, Comment, ProjectItem, …), or an Array of resources for a batch event (keyed by the pluralized element key, e.g. :project_items)

  • user (Object, nil) (defaults to: nil)

    explicit actor; falls back to config.current_user

  • extra (Hash)

    additional payload entries (changes:, labels:, user_ids:, …)



31
32
33
34
35
36
# File 'lib/plan_my_stuff/notifications.rb', line 31

def instrument(event, resource, user: nil, **extra)
  actor = user || resolve_current_user
  payload = build_payload(resource, actor, extra)
  log(event, payload)
  ActiveSupport::Notifications.instrument("#{event}.#{EVENT_SUFFIX}", payload)
end

.log(event, payload) ⇒ void

This method returns an undefined value.

Emits a debug log line for the event. No-op when no logger is available (e.g. outside Rails).

Parameters:

  • event (String)
  • payload (Hash)


118
119
120
121
122
123
# File 'lib/plan_my_stuff/notifications.rb', line 118

def log(event, payload)
  logger = rails_logger
  return if logger.nil?

  logger.debug { "[PlanMyStuff] #{event}.#{EVENT_SUFFIX} #{log_fields(payload)}" }
end

.log_fields(payload) ⇒ String

Parameters:

  • payload (Hash)

Returns:

  • (String)


137
138
139
140
141
142
143
144
145
146
# File 'lib/plan_my_stuff/notifications.rb', line 137

def log_fields(payload)
  fields = []
  fields << "user=#{payload[:user].inspect}" if payload.key?(:user)
  payload.each do |key, value|
    next if SKIPPED_LOG_KEYS.include?(key)

    fields << "#{key}=#{value.inspect}"
  end
  fields.join(' ')
end

.rails_loggerLogger?

Returns:

  • (Logger, nil)


126
127
128
129
130
131
# File 'lib/plan_my_stuff/notifications.rb', line 126

def rails_logger
  return unless defined?(Rails)
  return unless Rails.respond_to?(:logger)

  Rails.logger
end

.resolve_current_userObject?

Invokes config.current_user if it responds to call.

Returns:

  • (Object, nil)


42
43
44
45
46
47
# File 'lib/plan_my_stuff/notifications.rb', line 42

def resolve_current_user
  resolver = PlanMyStuff.configuration.current_user
  return if resolver.nil?

  resolver.respond_to?(:call) ? resolver.call : resolver
end

.visibility_fields(resource) ⇒ Hash

Extracts visibility + allowlist from Issue/Comment resources. Returns an empty hash for resources without visibility.

Parameters:

  • resource (Object)

Returns:

  • (Hash)


92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/plan_my_stuff/notifications.rb', line 92

def visibility_fields(resource)
  case resource
  when PlanMyStuff::Issue
    {
      visibility: resource..visibility,
      visibility_allowlist: Array.wrap(resource..visibility_allowlist),
    }
  when PlanMyStuff::Comment
    parent_allowlist = resource.issue ? resource.issue..visibility_allowlist : []
    {
      visibility: resource.visibility&.to_s,
      visibility_allowlist: Array.wrap(parent_allowlist),
    }
  else
    {}
  end
end