Class: PlanMyStuff::Comment

Inherits:
ApplicationRecord show all
Defined in:
lib/plan_my_stuff/comment.rb

Overview

Wraps a GitHub comment with parsed PMS metadata. Class methods provide the public API for CRUD operations.

Follows an ActiveRecord-style pattern:

  • ‘Comment.new(**attrs)` creates an unpersisted instance

  • ‘Comment.create!` / `Comment.list` return persisted instances

  • ‘comment.save!` / `comment.update!` / `comment.reload` for persistence

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

#new_record?, #persisted?

Constructor Details

#initialize(**attrs) ⇒ Comment

Returns a new instance of Comment.



179
180
181
182
183
184
# File 'lib/plan_my_stuff/comment.rb', line 179

def initialize(**attrs)
  @id = attrs.delete(:id)
  @raw_body = nil
  @metadata = CommentMetadata.new
  super
end

Instance Attribute Details

#bodyString

Returns comment body without the metadata HTML comment.

Returns:

  • (String)

    comment body without the metadata HTML comment



20
21
22
# File 'lib/plan_my_stuff/comment.rb', line 20

def body
  @body
end

#idInteger (readonly)

Returns GitHub comment ID.

Returns:

  • (Integer)

    GitHub comment ID



13
14
15
# File 'lib/plan_my_stuff/comment.rb', line 13

def id
  @id
end

#issuePlanMyStuff::Issue

Returns parent issue.

Returns:



22
23
24
# File 'lib/plan_my_stuff/comment.rb', line 22

def issue
  @issue
end

#metadataPlanMyStuff::CommentMetadata (readonly)

Returns parsed metadata (empty when no PMS metadata present).

Returns:



17
18
19
# File 'lib/plan_my_stuff/comment.rb', line 17

def 
  @metadata
end

#raw_bodyString (readonly)

Returns full body as stored on GitHub.

Returns:

  • (String)

    full body as stored on GitHub



15
16
17
# File 'lib/plan_my_stuff/comment.rb', line 15

def raw_body
  @raw_body
end

#visibilitySymbol?

Returns the comment visibility as a symbol. Uses the locally set value if present, otherwise falls back to metadata.

Returns:

  • (Symbol, nil)

    :public or :internal



254
255
256
# File 'lib/plan_my_stuff/comment.rb', line 254

def visibility
  @visibility || .visibility&.to_sym
end

Class Method Details

.create!(issue:, body:, user: nil, visibility: :public, custom_fields: {}, skip_responded: false, issue_body: false) ⇒ PlanMyStuff::Comment

Creates a comment on a GitHub issue with PMS metadata and a visible header.

Parameters:

  • issue (PlanMyStuff::Issue)

    parent issue

  • body (String)
  • user (Object, Integer) (defaults to: nil)

    user object or user_id

  • visibility (Symbol) (defaults to: :public)

    :public or :internal

  • custom_fields (Hash) (defaults to: {})
  • issue_body (Boolean) (defaults to: false)

    whether this comment holds the issue body

Returns:



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/plan_my_stuff/comment.rb', line 38

def create!(
  issue:,
  body:,
  user: nil,
  visibility: :public,
  custom_fields: {},
  skip_responded: false,
  issue_body: false
)
  resolved_user = UserResolver.resolve(user)
  visibility = resolve_visibility(visibility, resolved_user)
   = CommentMetadata.build(
    user: resolved_user,
    visibility: visibility.to_s,
    custom_fields: custom_fields,
    issue_body: issue_body,
  )

  header = build_header(resolved_user)
  full_body = "#{header}\n\n#{body}"
  serialized_body = MetadataParser.serialize(.to_h, full_body)

  result = PlanMyStuff.client.rest(:add_comment, issue.repo, issue.number, serialized_body)

  mark_issue_responded_if_first_support_comment(issue, resolved_user) unless skip_responded

  build(result, issue: issue)
end

.find(id, issue:) ⇒ PlanMyStuff::Comment

Finds a single comment by ID, given its parent issue.

Parameters:

Returns:



86
87
88
89
# File 'lib/plan_my_stuff/comment.rb', line 86

def find(id, issue:)
  github_comment = PlanMyStuff.client.rest(:issue_comment, issue.repo, id)
  build(github_comment, issue: issue)
end

.list(issue:, pms_only: false) ⇒ Array<PlanMyStuff::Comment>

Lists comments on a GitHub issue, optionally filtering to PMS-only comments.

Parameters:

Returns:



98
99
100
101
102
103
# File 'lib/plan_my_stuff/comment.rb', line 98

def list(issue:, pms_only: false)
  github_comments = PlanMyStuff.client.rest(:issue_comments, issue.repo, issue.number)
  comments = github_comments.map { |gc| build(gc, issue: issue) }

  pms_only ? comments.select(&:pms_comment?) : comments
end

.update!(id:, repo:, body:) ⇒ Object

Updates an existing GitHub comment body.

Parameters:

  • id (Integer)

    comment ID

  • repo (String)

    repo path

  • body (String)

    new serialized body

Returns:

  • (Object)

    Octokit response



75
76
77
# File 'lib/plan_my_stuff/comment.rb', line 75

def update!(id:, repo:, body:)
  PlanMyStuff.client.rest(:update_comment, repo, id, body)
end

Instance Method Details

#body_without_headerString

Returns the comment body with the header stripped.

Returns:

  • (String)


290
291
292
# File 'lib/plan_my_stuff/comment.rb', line 290

def body_without_header
  (body || '').sub(/\A###\s.+?:\s*\n\n/, '')
end

#headerString?

Extracts the ‘### Name at timestamp:` header line from the comment body.

Returns:

  • (String, nil)


281
282
283
284
# File 'lib/plan_my_stuff/comment.rb', line 281

def header
  match = (body || '').match(/\A(###\s.+?:\s*)\n/)
  match&.captures&.first&.strip
end

#pms_comment?Boolean

Returns:

  • (Boolean)


245
246
247
# File 'lib/plan_my_stuff/comment.rb', line 245

def pms_comment?
  .schema_version.present?
end

#reloadself

Re-fetches this comment from GitHub and updates all local attributes.

Returns:

  • (self)


238
239
240
241
242
# File 'lib/plan_my_stuff/comment.rb', line 238

def reload
  github_comment = PlanMyStuff.client.rest(:issue_comment, issue.repo, id)
  hydrate_from_github(github_comment, issue: issue)
  self
end

#save!self

Persists the comment. Creates if new, updates if persisted.

Returns:

  • (self)

Raises:



192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/plan_my_stuff/comment.rb', line 192

def save!
  if new_record?
    created = self.class.create!(
      issue: issue,
      body: body,
      visibility: visibility || :public,
    )
    hydrate_from_comment(created)
  else
    update!(body: body)
  end

  self
end

#update!(**attrs) ⇒ self

Updates this comment on GitHub. Raises StaleObjectError if the remote has been modified since this instance was loaded.

Parameters:

  • attrs (Hash)

    attributes to update (body:, visibility:)

Returns:

  • (self)

Raises:



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/plan_my_stuff/comment.rb', line 216

def update!(**attrs)
  raise_if_stale!

  new_body = attrs[:body] || body
  meta_hash = .to_h

  if attrs.key?(:visibility)
    new_visibility = attrs[:visibility].to_s
    meta_hash[:visibility] = new_visibility
    meta_hash[:updated_at] = Time.now.utc.iso8601
  end

  serialized = MetadataParser.serialize(meta_hash, new_body)
  self.class.update!(id: id, repo: issue.repo, body: serialized)

  reload
end

#visible_to?(user) ⇒ Boolean

Checks if the comment is visible to the given user. Public PMS comments: visible to everyone the parent issue is visible to. Internal PMS comments: visible only to support users. Non-PMS comments: visible only to support users.

Parameters:

  • user (Object, Integer)

    user object or user_id

Returns:

  • (Boolean)


267
268
269
270
271
272
273
274
275
# File 'lib/plan_my_stuff/comment.rb', line 267

def visible_to?(user)
  resolved = PMS::UserResolver.resolve(user)

  if pms_comment?
    issue.visible_to?(resolved) && (visibility != :internal || PMS::UserResolver.support?(resolved))
  else
    PMS::UserResolver.support?(resolved)
  end
end