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 plan_my_stuff.<event> namespace via ActiveSupport::Notifications. Subscribers run synchronously.

Constant Summary collapse

EVENT_PREFIX =
'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)


54
55
56
57
58
59
60
61
62
# File 'lib/plan_my_stuff/notifications.rb', line 54

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.

Parameters:

  • resource (Object)

Returns:

  • (Symbol)


70
71
72
73
74
75
76
77
# File 'lib/plan_my_stuff/notifications.rb', line 70

def infer_resource_key(resource)
  case resource
  when PlanMyStuff::Issue then :issue
  when PlanMyStuff::Comment then :comment
  when PlanMyStuff::BaseProjectItem then :project_item
  else :resource
  end
end

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

This method returns an undefined value.

Fires plan_my_stuff.<event> with a normalized payload.

Parameters:

  • event (String)

    e.g. ‘issue.created’

  • resource (Object)

    domain object (Issue, Comment, ProjectItem, …)

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

    explicit actor; falls back to config.current_user

  • extra (Hash)

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



28
29
30
31
32
33
# File 'lib/plan_my_stuff/notifications.rb', line 28

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_PREFIX}.#{event}", 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)


112
113
114
115
116
117
# File 'lib/plan_my_stuff/notifications.rb', line 112

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

  logger.debug { "[PMS] #{EVENT_PREFIX}.#{event} #{log_fields(payload)}" }
end

.log_fields(payload) ⇒ String

Parameters:

  • payload (Hash)

Returns:

  • (String)


131
132
133
134
135
136
137
138
139
140
# File 'lib/plan_my_stuff/notifications.rb', line 131

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)


120
121
122
123
124
125
# File 'lib/plan_my_stuff/notifications.rb', line 120

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)


39
40
41
42
43
44
# File 'lib/plan_my_stuff/notifications.rb', line 39

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)


86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/plan_my_stuff/notifications.rb', line 86

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