plan_my_stuff 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/lib/plan_my_stuff/base_project.rb +4 -175
- data/lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb +184 -0
- data/lib/plan_my_stuff/issue.rb +10 -1081
- data/lib/plan_my_stuff/issue_extractions/approvals.rb +370 -0
- data/lib/plan_my_stuff/issue_extractions/links.rb +525 -0
- data/lib/plan_my_stuff/issue_extractions/viewers.rb +75 -0
- data/lib/plan_my_stuff/issue_extractions/waiting.rb +148 -0
- data/lib/plan_my_stuff/version.rb +2 -2
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f89dac35aac34b782834f0f282cff8670d5aa5898c70d40bba296713c277b8dc
|
|
4
|
+
data.tar.gz: f065369ec7bbec7b04aa1908bc147b53b9e1041cd0ddea6c4888fe76166289ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d9e2fd53f3a4c81091253c5b10b8af9ea89ae2ca6db713f0545f814e67e4ee5a72e2a8fc53b570aeaaf16219aff0e7104d8ce6611adb612da0772560251bb94
|
|
7
|
+
data.tar.gz: 2d07daa9e21da4cb72ff55796db32de43ed86627d8e2bfab5311472b81c4db4d323573556a5a22f7d4cb4751675b2f6b0e6fc08998abda6e07b25d36be21bbc2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.1
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Nothing, just a version bump
|
|
8
|
+
|
|
9
|
+
## 0.10.0
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `Issue` slimmed from 1791 to 911 lines by extracting feature clusters into per-feature modules under a sibling
|
|
14
|
+
`PlanMyStuff::IssueExtractions::*` namespace, included into `Issue`. Public API unchanged (`issue.approve!`,
|
|
15
|
+
`issue.add_related!`, `issue.enter_waiting_on_user!`, `issue.add_viewers!`, etc. still resolve to the same methods).
|
|
16
|
+
- `PlanMyStuff::IssueExtractions::Approvals` - `lib/plan_my_stuff/issue_extractions/approvals.rb`
|
|
17
|
+
- `PlanMyStuff::IssueExtractions::Links` - `lib/plan_my_stuff/issue_extractions/links.rb`
|
|
18
|
+
- `PlanMyStuff::IssueExtractions::Waiting` - `lib/plan_my_stuff/issue_extractions/waiting.rb`
|
|
19
|
+
- `PlanMyStuff::IssueExtractions::Viewers` - `lib/plan_my_stuff/issue_extractions/viewers.rb`
|
|
20
|
+
- `BaseProject` slimmed from 661 to 504 lines by extracting GraphQL hydration helpers into
|
|
21
|
+
`PlanMyStuff::BaseProjectExtractions::GraphqlHydration` (`lib/plan_my_stuff/base_project_extractions/graphql_hydration.rb`),
|
|
22
|
+
included into `BaseProject`'s singleton class. `BaseProject.find` / `BaseProject.list` remain public entry points.
|
|
23
|
+
- Specs for `Issue` features now live alongside the modules at `spec/plan_my_stuff/issue_extractions/<feature>_spec.rb`.
|
|
24
|
+
|
|
3
25
|
## 0.9.0
|
|
4
26
|
|
|
5
27
|
### Breaking
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'base_project_extractions/graphql_hydration'
|
|
4
|
+
|
|
3
5
|
module PlanMyStuff
|
|
4
6
|
# Shared base for GitHub Projects V2 wrappers. Holds attribute definitions, generic find/list/update machinery,
|
|
5
7
|
# hydration, and instance helpers. Concrete subclasses (Project, TestingProject) add their own +create!+ behavior
|
|
@@ -43,6 +45,8 @@ module PlanMyStuff
|
|
|
43
45
|
attribute :has_next_page
|
|
44
46
|
|
|
45
47
|
class << self
|
|
48
|
+
include PlanMyStuff::BaseProjectExtractions::GraphqlHydration
|
|
49
|
+
|
|
46
50
|
# Generic find - returns whichever concrete project type is at the given number, dispatching on metadata kind.
|
|
47
51
|
# Subclasses may override to apply filtering (e.g. Project raises for testing projects by default).
|
|
48
52
|
#
|
|
@@ -180,47 +184,6 @@ module PlanMyStuff
|
|
|
180
184
|
|
|
181
185
|
private
|
|
182
186
|
|
|
183
|
-
# Builds a summary Project from a list query node. Dispatches to TestingProject when the readme metadata has
|
|
184
|
-
# kind: "testing".
|
|
185
|
-
#
|
|
186
|
-
# @param node [Hash]
|
|
187
|
-
#
|
|
188
|
-
# @return [PlanMyStuff::BaseProject]
|
|
189
|
-
#
|
|
190
|
-
def build_summary(node)
|
|
191
|
-
raw_readme = node[:readme] || ''
|
|
192
|
-
parsed_meta = PlanMyStuff::MetadataParser.parse(raw_readme)
|
|
193
|
-
klass = dispatch_project_class(parsed_meta[:metadata])
|
|
194
|
-
project = klass.new
|
|
195
|
-
project.__send__(:hydrate_summary, node, raw_readme: raw_readme, parsed_meta: parsed_meta)
|
|
196
|
-
project
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
# Builds a detailed Project from a find query response. Dispatches to TestingProject when the readme metadata
|
|
200
|
-
# has kind: "testing".
|
|
201
|
-
#
|
|
202
|
-
# @param graphql_project [Hash]
|
|
203
|
-
# @param items [Array<Hash>]
|
|
204
|
-
# @param next_cursor [String, nil]
|
|
205
|
-
# @param has_next_page [Boolean, nil]
|
|
206
|
-
#
|
|
207
|
-
# @return [PlanMyStuff::BaseProject]
|
|
208
|
-
#
|
|
209
|
-
def build_detail(graphql_project, items:, next_cursor: nil, has_next_page: nil)
|
|
210
|
-
raw_readme = graphql_project[:readme] || ''
|
|
211
|
-
parsed_meta = PlanMyStuff::MetadataParser.parse(raw_readme)
|
|
212
|
-
klass = dispatch_project_class(parsed_meta[:metadata])
|
|
213
|
-
project = klass.new
|
|
214
|
-
project.__send__(
|
|
215
|
-
:hydrate_detail,
|
|
216
|
-
graphql_project,
|
|
217
|
-
items: items,
|
|
218
|
-
next_cursor: next_cursor,
|
|
219
|
-
has_next_page: has_next_page,
|
|
220
|
-
)
|
|
221
|
-
project
|
|
222
|
-
end
|
|
223
|
-
|
|
224
187
|
# Returns the appropriate project class based on the metadata kind field. Always dispatches to a concrete
|
|
225
188
|
# subclass (never BaseProject itself).
|
|
226
189
|
#
|
|
@@ -234,140 +197,6 @@ module PlanMyStuff
|
|
|
234
197
|
PlanMyStuff::Project
|
|
235
198
|
end
|
|
236
199
|
|
|
237
|
-
# @param org [String]
|
|
238
|
-
# @param number [Integer]
|
|
239
|
-
#
|
|
240
|
-
# @return [PlanMyStuff::BaseProject]
|
|
241
|
-
#
|
|
242
|
-
def find_auto_paginated(org, number)
|
|
243
|
-
all_items = []
|
|
244
|
-
cursor = nil
|
|
245
|
-
raw_project = nil
|
|
246
|
-
page = nil
|
|
247
|
-
|
|
248
|
-
loop do
|
|
249
|
-
page = fetch_project_page(org, number, cursor)
|
|
250
|
-
raw_project ||= page[:raw]
|
|
251
|
-
all_items.concat(page[:items])
|
|
252
|
-
|
|
253
|
-
break if !page[:has_next_page] || all_items.length >= MAX_AUTO_PAGINATE_ITEMS
|
|
254
|
-
|
|
255
|
-
cursor = page[:next_cursor]
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
build_detail(
|
|
259
|
-
raw_project,
|
|
260
|
-
items: all_items,
|
|
261
|
-
next_cursor: page[:next_cursor],
|
|
262
|
-
has_next_page: page[:has_next_page],
|
|
263
|
-
)
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# @param org [String]
|
|
267
|
-
# @param number [Integer]
|
|
268
|
-
# @param cursor [String, nil]
|
|
269
|
-
#
|
|
270
|
-
# @return [PlanMyStuff::BaseProject]
|
|
271
|
-
#
|
|
272
|
-
def find_with_cursor(org, number, cursor:)
|
|
273
|
-
page = fetch_project_page(org, number, cursor)
|
|
274
|
-
build_detail(
|
|
275
|
-
page[:raw],
|
|
276
|
-
items: page[:items],
|
|
277
|
-
next_cursor: page[:next_cursor],
|
|
278
|
-
has_next_page: page[:has_next_page],
|
|
279
|
-
)
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
# Fetches a single page of project data. Returns a lightweight hash for pagination loop consumption (not a
|
|
283
|
-
# Project instance).
|
|
284
|
-
#
|
|
285
|
-
# @param org [String]
|
|
286
|
-
# @param number [Integer]
|
|
287
|
-
# @param cursor [String, nil]
|
|
288
|
-
#
|
|
289
|
-
# @return [Hash] with :raw, :items, :next_cursor, :has_next_page
|
|
290
|
-
#
|
|
291
|
-
def fetch_project_page(org, number, cursor)
|
|
292
|
-
variables = { org: org, number: number }
|
|
293
|
-
variables[:cursor] = cursor if cursor
|
|
294
|
-
|
|
295
|
-
data = PlanMyStuff.client.graphql(
|
|
296
|
-
PlanMyStuff::GraphQL::Queries::FIND_PROJECT,
|
|
297
|
-
variables: variables,
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
raw_project = data.dig(:organization, :projectV2)
|
|
301
|
-
page_info = raw_project.dig(:items, :pageInfo) || {}
|
|
302
|
-
items_data = raw_project.dig(:items, :nodes) || []
|
|
303
|
-
|
|
304
|
-
{
|
|
305
|
-
raw: raw_project,
|
|
306
|
-
items: items_data.map { |item| parse_project_item(item) },
|
|
307
|
-
next_cursor: page_info[:endCursor],
|
|
308
|
-
has_next_page: page_info[:hasNextPage],
|
|
309
|
-
}
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
# @param item [Hash] raw GraphQL project item node
|
|
313
|
-
#
|
|
314
|
-
# @return [Hash]
|
|
315
|
-
#
|
|
316
|
-
def parse_project_item(item)
|
|
317
|
-
content = item[:content] || {}
|
|
318
|
-
field_values = item.dig(:fieldValues, :nodes) || []
|
|
319
|
-
repo_name = content.dig(:repository, :nameWithOwner)
|
|
320
|
-
|
|
321
|
-
{
|
|
322
|
-
id: item[:id],
|
|
323
|
-
type: item[:type],
|
|
324
|
-
content_node_id: content[:id],
|
|
325
|
-
title: content[:title],
|
|
326
|
-
body: content[:body],
|
|
327
|
-
number: content[:number],
|
|
328
|
-
url: content[:url],
|
|
329
|
-
state: content[:state],
|
|
330
|
-
repo: repo_name.present? ? PlanMyStuff::Repo.resolve!(repo_name) : nil,
|
|
331
|
-
status: extract_item_status(field_values),
|
|
332
|
-
field_values: parse_field_values(field_values),
|
|
333
|
-
updated_at: item[:updatedAt],
|
|
334
|
-
github_response: item,
|
|
335
|
-
}
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
# @param field_values [Array<Hash>]
|
|
339
|
-
#
|
|
340
|
-
# @return [String, nil]
|
|
341
|
-
#
|
|
342
|
-
def extract_item_status(field_values)
|
|
343
|
-
status_value = field_values.find { |fv| fv.dig(:field, :name) == 'Status' }
|
|
344
|
-
|
|
345
|
-
status_value&.dig(:name)
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
# @param field_values [Array<Hash>]
|
|
349
|
-
#
|
|
350
|
-
# @return [Hash]
|
|
351
|
-
#
|
|
352
|
-
def parse_field_values(field_values)
|
|
353
|
-
result = {}
|
|
354
|
-
|
|
355
|
-
field_values.each do |fv|
|
|
356
|
-
field_name = fv.dig(:field, :name)
|
|
357
|
-
next unless field_name
|
|
358
|
-
|
|
359
|
-
value = fv[:name] || fv[:text]
|
|
360
|
-
users_node = fv[:users]
|
|
361
|
-
if users_node
|
|
362
|
-
value = (users_node[:nodes] || []).map { |u| u[:login] }
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
result[field_name] = value
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
result
|
|
369
|
-
end
|
|
370
|
-
|
|
371
200
|
# Resolves a project number to its node ID.
|
|
372
201
|
#
|
|
373
202
|
# @param org [String]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
module BaseProjectExtractions
|
|
5
|
+
module GraphqlHydration
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# Builds a summary Project from a list query node. Dispatches to TestingProject when the readme metadata has
|
|
9
|
+
# kind: "testing".
|
|
10
|
+
#
|
|
11
|
+
# @param node [Hash]
|
|
12
|
+
#
|
|
13
|
+
# @return [PlanMyStuff::BaseProject]
|
|
14
|
+
#
|
|
15
|
+
def build_summary(node)
|
|
16
|
+
raw_readme = node[:readme] || ''
|
|
17
|
+
parsed_meta = PlanMyStuff::MetadataParser.parse(raw_readme)
|
|
18
|
+
klass = dispatch_project_class(parsed_meta[:metadata])
|
|
19
|
+
project = klass.new
|
|
20
|
+
project.__send__(:hydrate_summary, node, raw_readme: raw_readme, parsed_meta: parsed_meta)
|
|
21
|
+
project
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Builds a detailed Project from a find query response. Dispatches to TestingProject when the readme metadata
|
|
25
|
+
# has kind: "testing".
|
|
26
|
+
#
|
|
27
|
+
# @param graphql_project [Hash]
|
|
28
|
+
# @param items [Array<Hash>]
|
|
29
|
+
# @param next_cursor [String, nil]
|
|
30
|
+
# @param has_next_page [Boolean, nil]
|
|
31
|
+
#
|
|
32
|
+
# @return [PlanMyStuff::BaseProject]
|
|
33
|
+
#
|
|
34
|
+
def build_detail(graphql_project, items:, next_cursor: nil, has_next_page: nil)
|
|
35
|
+
raw_readme = graphql_project[:readme] || ''
|
|
36
|
+
parsed_meta = PlanMyStuff::MetadataParser.parse(raw_readme)
|
|
37
|
+
klass = dispatch_project_class(parsed_meta[:metadata])
|
|
38
|
+
project = klass.new
|
|
39
|
+
project.__send__(
|
|
40
|
+
:hydrate_detail,
|
|
41
|
+
graphql_project,
|
|
42
|
+
items: items,
|
|
43
|
+
next_cursor: next_cursor,
|
|
44
|
+
has_next_page: has_next_page,
|
|
45
|
+
)
|
|
46
|
+
project
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param org [String]
|
|
50
|
+
# @param number [Integer]
|
|
51
|
+
#
|
|
52
|
+
# @return [PlanMyStuff::BaseProject]
|
|
53
|
+
#
|
|
54
|
+
def find_auto_paginated(org, number)
|
|
55
|
+
all_items = []
|
|
56
|
+
cursor = nil
|
|
57
|
+
raw_project = nil
|
|
58
|
+
page = nil
|
|
59
|
+
|
|
60
|
+
loop do
|
|
61
|
+
page = fetch_project_page(org, number, cursor)
|
|
62
|
+
raw_project ||= page[:raw]
|
|
63
|
+
all_items.concat(page[:items])
|
|
64
|
+
|
|
65
|
+
break if !page[:has_next_page] || all_items.length >= PlanMyStuff::BaseProject::MAX_AUTO_PAGINATE_ITEMS
|
|
66
|
+
|
|
67
|
+
cursor = page[:next_cursor]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
build_detail(
|
|
71
|
+
raw_project,
|
|
72
|
+
items: all_items,
|
|
73
|
+
next_cursor: page[:next_cursor],
|
|
74
|
+
has_next_page: page[:has_next_page],
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param org [String]
|
|
79
|
+
# @param number [Integer]
|
|
80
|
+
# @param cursor [String, nil]
|
|
81
|
+
#
|
|
82
|
+
# @return [PlanMyStuff::BaseProject]
|
|
83
|
+
#
|
|
84
|
+
def find_with_cursor(org, number, cursor:)
|
|
85
|
+
page = fetch_project_page(org, number, cursor)
|
|
86
|
+
build_detail(
|
|
87
|
+
page[:raw],
|
|
88
|
+
items: page[:items],
|
|
89
|
+
next_cursor: page[:next_cursor],
|
|
90
|
+
has_next_page: page[:has_next_page],
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Fetches a single page of project data. Returns a lightweight hash for pagination loop consumption (not a
|
|
95
|
+
# Project instance).
|
|
96
|
+
#
|
|
97
|
+
# @param org [String]
|
|
98
|
+
# @param number [Integer]
|
|
99
|
+
# @param cursor [String, nil]
|
|
100
|
+
#
|
|
101
|
+
# @return [Hash] with :raw, :items, :next_cursor, :has_next_page
|
|
102
|
+
#
|
|
103
|
+
def fetch_project_page(org, number, cursor)
|
|
104
|
+
variables = { org: org, number: number }
|
|
105
|
+
variables[:cursor] = cursor if cursor
|
|
106
|
+
|
|
107
|
+
data = PlanMyStuff.client.graphql(
|
|
108
|
+
PlanMyStuff::GraphQL::Queries::FIND_PROJECT,
|
|
109
|
+
variables: variables,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
raw_project = data.dig(:organization, :projectV2)
|
|
113
|
+
page_info = raw_project.dig(:items, :pageInfo) || {}
|
|
114
|
+
items_data = raw_project.dig(:items, :nodes) || []
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
raw: raw_project,
|
|
118
|
+
items: items_data.map { |item| parse_project_item(item) },
|
|
119
|
+
next_cursor: page_info[:endCursor],
|
|
120
|
+
has_next_page: page_info[:hasNextPage],
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @param item [Hash] raw GraphQL project item node
|
|
125
|
+
#
|
|
126
|
+
# @return [Hash]
|
|
127
|
+
#
|
|
128
|
+
def parse_project_item(item)
|
|
129
|
+
content = item[:content] || {}
|
|
130
|
+
field_values = item.dig(:fieldValues, :nodes) || []
|
|
131
|
+
repo_name = content.dig(:repository, :nameWithOwner)
|
|
132
|
+
|
|
133
|
+
{
|
|
134
|
+
id: item[:id],
|
|
135
|
+
type: item[:type],
|
|
136
|
+
content_node_id: content[:id],
|
|
137
|
+
title: content[:title],
|
|
138
|
+
body: content[:body],
|
|
139
|
+
number: content[:number],
|
|
140
|
+
url: content[:url],
|
|
141
|
+
state: content[:state],
|
|
142
|
+
repo: repo_name.present? ? PlanMyStuff::Repo.resolve!(repo_name) : nil,
|
|
143
|
+
status: extract_item_status(field_values),
|
|
144
|
+
field_values: parse_field_values(field_values),
|
|
145
|
+
updated_at: item[:updatedAt],
|
|
146
|
+
github_response: item,
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# @param field_values [Array<Hash>]
|
|
151
|
+
#
|
|
152
|
+
# @return [String, nil]
|
|
153
|
+
#
|
|
154
|
+
def extract_item_status(field_values)
|
|
155
|
+
status_value = field_values.find { |fv| fv.dig(:field, :name) == 'Status' }
|
|
156
|
+
|
|
157
|
+
status_value&.dig(:name)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @param field_values [Array<Hash>]
|
|
161
|
+
#
|
|
162
|
+
# @return [Hash]
|
|
163
|
+
#
|
|
164
|
+
def parse_field_values(field_values)
|
|
165
|
+
result = {}
|
|
166
|
+
|
|
167
|
+
field_values.each do |fv|
|
|
168
|
+
field_name = fv.dig(:field, :name)
|
|
169
|
+
next unless field_name
|
|
170
|
+
|
|
171
|
+
value = fv[:name] || fv[:text]
|
|
172
|
+
users_node = fv[:users]
|
|
173
|
+
if users_node
|
|
174
|
+
value = (users_node[:nodes] || []).map { |u| u[:login] }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
result[field_name] = value
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
result
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|