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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6457257ef431e923519943cd227ffc70488b023467253873e20f52c47fd75f52
4
- data.tar.gz: b5a16da4dc6c6c3ce45b307879f5146720f2b878809b7a071b7cc9e33986d210
3
+ metadata.gz: f89dac35aac34b782834f0f282cff8670d5aa5898c70d40bba296713c277b8dc
4
+ data.tar.gz: f065369ec7bbec7b04aa1908bc147b53b9e1041cd0ddea6c4888fe76166289ee
5
5
  SHA512:
6
- metadata.gz: efee5f594d9bd014b0ce575781790768a8d51277010d9ad9902ac11beea06c17d39232d65a77532768326eccece3ebcbe3bdb18c464341c963eda9b12b1d6075
7
- data.tar.gz: ef94bc70d08874c2347e243d0f7967ca7bca2e631cda775494a714df6501d22fe5011a09db805dbb53f5fe8397eb32b8bdf19bbf8a235da39d3f5dd4810c011a
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