checkoff 0.257.0 → 0.260.0

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/lib/checkoff/attachments.rb +14 -15
  3. data/lib/checkoff/cli.rb +7 -12
  4. data/lib/checkoff/clients.rb +0 -2
  5. data/lib/checkoff/custom_fields.rb +13 -26
  6. data/lib/checkoff/internal/asana_event_enrichment.rb +11 -13
  7. data/lib/checkoff/internal/asana_event_filter.rb +1 -1
  8. data/lib/checkoff/internal/config_loader.rb +0 -2
  9. data/lib/checkoff/internal/logging.rb +0 -2
  10. data/lib/checkoff/internal/project_hashes.rb +5 -4
  11. data/lib/checkoff/internal/search_url/custom_field_param_converter.rb +9 -9
  12. data/lib/checkoff/internal/search_url/custom_field_variant.rb +7 -0
  13. data/lib/checkoff/internal/search_url/date_param_converter.rb +13 -4
  14. data/lib/checkoff/internal/search_url/parser.rb +2 -7
  15. data/lib/checkoff/internal/search_url/simple_param_converter.rb +11 -5
  16. data/lib/checkoff/internal/section_selector_evaluator.rb +1 -1
  17. data/lib/checkoff/internal/selector_classes/common/function_evaluator.rb +5 -2
  18. data/lib/checkoff/internal/selector_classes/common.rb +13 -31
  19. data/lib/checkoff/internal/selector_classes/function_evaluator.rb +20 -4
  20. data/lib/checkoff/internal/selector_classes/project/function_evaluator.rb +11 -2
  21. data/lib/checkoff/internal/selector_classes/project.rb +5 -13
  22. data/lib/checkoff/internal/selector_classes/section/function_evaluator.rb +8 -5
  23. data/lib/checkoff/internal/selector_classes/section.rb +2 -5
  24. data/lib/checkoff/internal/selector_classes/task/function_evaluator.rb +2 -2
  25. data/lib/checkoff/internal/selector_classes/task.rb +24 -23
  26. data/lib/checkoff/internal/selector_evaluator.rb +8 -9
  27. data/lib/checkoff/internal/task_hashes.rb +17 -9
  28. data/lib/checkoff/internal/task_timing.rb +0 -4
  29. data/lib/checkoff/internal/thread_local.rb +0 -2
  30. data/lib/checkoff/monkeypatches/resource_marshalling.rb +3 -3
  31. data/lib/checkoff/my_tasks.rb +1 -4
  32. data/lib/checkoff/portfolios.rb +0 -1
  33. data/lib/checkoff/project_selectors.rb +1 -1
  34. data/lib/checkoff/projects.rb +19 -8
  35. data/lib/checkoff/resources.rb +14 -3
  36. data/lib/checkoff/sections.rb +13 -19
  37. data/lib/checkoff/subtasks.rb +1 -2
  38. data/lib/checkoff/tags.rb +3 -4
  39. data/lib/checkoff/task_searches.rb +1 -8
  40. data/lib/checkoff/task_selectors.rb +25 -12
  41. data/lib/checkoff/tasks.rb +8 -16
  42. data/lib/checkoff/timelines.rb +5 -10
  43. data/lib/checkoff/timing.rb +1 -2
  44. data/lib/checkoff/version.rb +1 -1
  45. data/lib/checkoff/workspaces.rb +1 -7
  46. data/rbi/checkoff.rbi +122 -179
  47. data/sig/checkoff.rbs +83 -181
  48. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1896715fc41d51e76c6c1381eed1a4b3cd72ee304a75b36d1a26bfe42d0ee158
4
- data.tar.gz: 07f5cc66320360ebc668d21adc062585fb6927b029872b99f0416f30b7986387
3
+ metadata.gz: e08117848a46a27ffee42a7936ada4febefdda2c2d9a44ae0160dcdfa89989b8
4
+ data.tar.gz: a8573c8961ab241c73425008c5c9a44f03c6038a4a3ad791b3154b505cd4ee22
5
5
  SHA512:
6
- metadata.gz: 161d6849b466f214b62ee2c04d2188bd672323560ff37fd99709166efb920aea03aec8f5255835b2b938ee6c0036c847e40d57a509ccaeded3f03b5bda98d603
7
- data.tar.gz: 16acaccaea9884fbfe513ac8dfe6f9bb30f72342deba8d88a335a3f391d8fb18fd61d8034731346b5e6015ef9c0fb125e3153599dfe2eeb76dfd4ed6ff0ccfe3
6
+ metadata.gz: 43b8c4f4ff4d1fcd48a98d0fe2b7fdc6332ab29e83b81f2dad3a9a9ec3628f3c106c4ea518ae81b0e9edc882bf9618396b547c099e6dc1fb22f76d5c91ed6cd5
7
+ data.tar.gz: 2da763b8482d3e7a6f7e8fe8b912ef2c8175d6843da3019fc10a7aae9ea94d3978a9ed789763f5fee8d7755200f80bd04268b332ab74b2865fd6303823c99e34
@@ -85,9 +85,10 @@ module Checkoff
85
85
  # @return [Object]
86
86
  def download_uri(uri, verify_mode: OpenSSL::SSL::VERIFY_PEER, &block)
87
87
  out = nil
88
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', verify_mode:) do |http|
89
- # @sg-ignore Unresolved constant Net::HTTP::Get / Unresolved call to request
90
- # stdlib RBS gap on Net::HTTP block param types
88
+ host = uri.host || raise("URI has no host: #{uri}")
89
+ Net::HTTP.start(host, uri.port, use_ssl: uri.scheme == 'https', verify_mode:) do |http|
90
+ # @sg-ignore tool-limitation:pr-1290-follow-up
91
+ # https://github.com/castwide/solargraph/pull/1290#issuecomment-5272957964
91
92
  http.request(Net::HTTP::Get.new(uri)) do |response|
92
93
  raise("Unexpected response code: #{response.code}") unless response.code == '200'
93
94
 
@@ -108,7 +109,6 @@ module Checkoff
108
109
  def write_tempfile_from_response(response)
109
110
  Tempfile.create('checkoff') do |tempfile|
110
111
  tempfile.binmode
111
- # @sg-ignore Unresolved call to read_body on #read_body
112
112
  response.read_body do |chunk|
113
113
  tempfile.write(chunk)
114
114
  end
@@ -128,16 +128,15 @@ module Checkoff
128
128
  def create_attachment_from_downloaded_url!(url, resource, attachment_name:,
129
129
  verify_mode: OpenSSL::SSL::VERIFY_PEER)
130
130
  uri = URI(url)
131
- attachment_name ||= File.basename(uri.path)
131
+ # @type [String]
132
+ resolved_attachment_name = attachment_name || File.basename(uri.path || '')
132
133
  download_uri(uri, verify_mode:) do |tempfile|
133
- # @sg-ignore attachment_name reassignment above isn't narrowed from the declared
134
- # [String, nil] param type — needs a fresh typed local, deferred to a follow-up code PR
135
- content_type ||= content_type_from_filename(attachment_name)
136
- # @sg-ignore URI::Generic#path can be nil
137
- content_type ||= content_type_from_filename(uri.path)
138
-
139
- # @sg-ignore same attachment_name/content_type narrowing gap as above
140
- resource.attach(filename: attachment_name, mime: content_type,
134
+ content_type = content_type_from_filename(resolved_attachment_name)
135
+ content_type ||= content_type_from_filename(uri.path || '')
136
+ # neither the attachment name nor the URL's path has a MIME-recognizable extension
137
+ content_type ||= 'application/octet-stream'
138
+
139
+ resource.attach(filename: resolved_attachment_name, mime: content_type,
141
140
  io: tempfile)
142
141
  end
143
142
  end
@@ -196,8 +195,8 @@ module Checkoff
196
195
  tasks = Checkoff::Tasks.new
197
196
  attachments = Checkoff::Attachments.new
198
197
  task = tasks.task_by_gid(gid)
199
- # @sg-ignore task_by_gid can return nil; needs a raise-if-nil guard, deferred to a
200
- # follow-up code PR
198
+ raise "Could not find task #{gid}" if task.nil?
199
+
201
200
  attachment = attachments.create_attachment_from_url!(url, task)
202
201
  puts "Results: #{attachment.inspect}"
203
202
  end
data/lib/checkoff/cli.rb CHANGED
@@ -161,7 +161,7 @@ module Checkoff
161
161
  # @param workspace_name [String]
162
162
  # @param project_name [String, Symbol]
163
163
  # @param section_name [String, Symbol, nil]
164
- # @param task_name [String, Symbol, nil]
164
+ # @param task_name [String, nil]
165
165
  # @param config [Checkoff::Internal::EnvFallbackConfigLoader, Hash]
166
166
  # @param projects [Checkoff::Projects]
167
167
  # @param sections [Checkoff::Sections]
@@ -192,7 +192,6 @@ module Checkoff
192
192
  elsif task_name.nil?
193
193
  run_on_section(workspace_name, project_name, section_name)
194
194
  else
195
- # @sg-ignore
196
195
  run_on_task(workspace_name, project_name, section_name, task_name)
197
196
  end
198
197
  end
@@ -243,7 +242,6 @@ module Checkoff
243
242
  task = tasks.task(workspace.to_s, project, task_name, section_name: section)
244
243
  raise "Task not found: #{task_name}" if task.nil?
245
244
 
246
- # @sg-ignore nil check above is not flow-sensitive
247
245
  task_to_hash(task).to_json
248
246
  end
249
247
 
@@ -311,8 +309,9 @@ module Checkoff
311
309
  desc 'Add a short task to Asana'
312
310
  arg 'workspace'
313
311
  arg 'task_name'
312
+ # @param c [GLI::Command]
314
313
  command :quickadd do |c|
315
- # @sg-ignore
314
+ # @param args [Array<String>]
316
315
  c.action do |_global_options, _options, args|
317
316
  workspace_name = args.fetch(0)
318
317
  task_name = args.fetch(1)
@@ -326,8 +325,9 @@ module Checkoff
326
325
  arg 'project'
327
326
  arg 'section', :optional
328
327
  arg 'task_name', :optional
328
+ # @param c [GLI::Command]
329
329
  command :view do |c|
330
- # @sg-ignore
330
+ # @param args [Array<String>]
331
331
  c.action do |_global_options, _options, args|
332
332
  workspace_name = args.fetch(0)
333
333
  project_name = args.fetch(1)
@@ -341,38 +341,33 @@ module Checkoff
341
341
  desc 'Move tasks from one section to another within a project'
342
342
 
343
343
  # rubocop:disable Metrics/BlockLength
344
+ # @param c [GLI::Command]
344
345
  command :mv do |c|
345
- # @sg-ignore
346
346
  c.flag :from_workspace,
347
347
  type: String,
348
348
  default_value: :default_workspace,
349
349
  desc: 'Workspace to move tasks from'
350
- # @sg-ignore
351
350
  c.flag :from_project,
352
351
  type: String,
353
352
  required: true,
354
353
  desc: 'Project to move tasks from'
355
- # @sg-ignore
356
354
  c.flag :from_section,
357
355
  type: String,
358
356
  default_value: :all_sections,
359
357
  desc: 'Section to move tasks from'
360
- # @sg-ignore
361
358
  c.flag :to_workspace,
362
359
  type: String,
363
360
  default_value: :source_workspace,
364
361
  desc: 'Workspace to move tasks to'
365
- # @sg-ignore
366
362
  c.flag :to_project,
367
363
  type: String,
368
364
  default_value: :source_project,
369
365
  desc: 'Section to move tasks to'
370
- # @sg-ignore
371
366
  c.flag :to_section,
372
367
  type: String,
373
368
  default_value: :source_section,
374
369
  desc: 'Section to move tasks to'
375
- # @sg-ignore
370
+ # @param options [Hash]
376
371
  c.action do |_global_options, options, _args|
377
372
  from_workspace = options.fetch('from_workspace')
378
373
  from_project = options.fetch('from_project')
@@ -30,9 +30,7 @@ module Checkoff
30
30
  # @return [Asana::Client]
31
31
  def client
32
32
  @client ||= @asana_client_class.new do |c|
33
- # @sg-ignore Unresolved call to authentication — gem config DSL block, not covered by annotations
34
33
  c.authentication :access_token, @config.fetch(:personal_access_token)
35
- # @sg-ignore Unresolved call to default_headers — gem config DSL block, not covered by annotations
36
34
  c.default_headers 'asana-enable' =>
37
35
  'new_project_templates,new_user_task_lists,new_memberships,new_goal_memberships'
38
36
  end
@@ -41,7 +41,6 @@ module Checkoff
41
41
  # @param custom_field_name [String]
42
42
  #
43
43
  # @return [Asana::Resources::CustomField]
44
- # @sg-ignore nil check above is not flow-sensitive
45
44
  def custom_field_or_raise(workspace_name, custom_field_name)
46
45
  cf = custom_field(workspace_name, custom_field_name)
47
46
  raise "Could not find custom_field #{custom_field_name} under workspace #{workspace_name}." if cf.nil?
@@ -79,40 +78,32 @@ module Checkoff
79
78
  # @param resource [Asana::Resources::Project,Asana::Resources::Task]
80
79
  # @param custom_field_name [String]
81
80
  # @return [Array<String>]
82
- # @sg-ignore Declared return type Array<String> does not match inferred type Array,Array<Array> —
83
- # fixing cleanly needs binding enum_value.fetch('name') to a typed local; leaving for a
84
- # follow-up code PR to keep this one annotation-only.
85
81
  def resource_custom_field_values_names_by_name(resource, custom_field_name)
86
82
  custom_field = resource_custom_field_by_name(resource, custom_field_name)
87
83
  return [] if custom_field.nil?
88
84
 
89
85
  resource_custom_field_enum_values(custom_field).flat_map do |enum_value|
90
- if enum_value.nil?
91
- []
92
- else
93
- [enum_value.fetch('name')]
94
- end
86
+ [enum_value.fetch('name')]
95
87
  end
96
88
  end
97
89
 
98
90
  # @param resource [Asana::Resources::Task,Asana::Resources::Project]
99
91
  # @param custom_field_name [String]
100
- # @return [Hash, nil]
92
+ # @return [Hash{String => Object}, nil]
101
93
  def resource_custom_field_by_name(resource, custom_field_name)
102
- # @type [Array<Hash>]
94
+ # @type [Array<Hash{String => Object}>]
103
95
  custom_fields = resource.custom_fields
104
96
  if custom_fields.nil?
105
97
  raise "custom fields not found on resource - did you add 'custom_fields' in your extra_fields argument?"
106
98
  end
107
99
 
108
- # @type [Hash, nil]
100
+ # @type [Hash{String => Object}, nil]
109
101
  custom_fields.find { |field| field.fetch('name') == custom_field_name }
110
102
  end
111
103
 
112
104
  # @param resource [Asana::Resources::Task,Asana::Resources::Project]
113
105
  # @param custom_field_name [String]
114
- # @return [Hash]
115
- # @sg-ignore nil check above is not flow-sensitive
106
+ # @return [Hash{String => Object}]
116
107
  def resource_custom_field_by_name_or_raise(resource, custom_field_name)
117
108
  custom_field = resource_custom_field_by_name(resource, custom_field_name)
118
109
  if custom_field.nil?
@@ -124,16 +115,15 @@ module Checkoff
124
115
 
125
116
  # @param resource [Asana::Resources::Project,Asana::Resources::Task]
126
117
  # @param custom_field_gid [String]
127
- # @return [Hash]
128
- # @sg-ignore nil check above is not flow-sensitive
118
+ # @return [Hash{String => Object}]
129
119
  def resource_custom_field_by_gid_or_raise(resource, custom_field_gid)
130
- # @type [Array<Hash>]
120
+ # @type [Array<Hash{String => Object}>]
131
121
  custom_fields = resource.custom_fields
132
122
  if custom_fields.nil?
133
123
  raise "Could not find custom_fields under project (was 'custom_fields' included in 'extra_fields'?)"
134
124
  end
135
125
 
136
- # @type [Hash, nil]
126
+ # @type [Hash{String => Object}, nil]
137
127
  matched_custom_field = custom_fields.find { |data| data.fetch('gid') == custom_field_gid }
138
128
  if matched_custom_field.nil?
139
129
  raise "Could not find custom field with gid #{custom_field_gid} " \
@@ -145,27 +135,24 @@ module Checkoff
145
135
 
146
136
  private
147
137
 
148
- # @param custom_field [Hash{String => Hash,Array<Hash>}]
138
+ # rubocop:disable Layout/LineLength, YARD/TagTypeSyntax
139
+ # @param custom_field [Hash{"resource_subtype" => String} & Hash{"enum_value" => Hash} & Hash{"multi_enum_values" => Array<Hash>}]
149
140
  #
150
- # @return [Array<Hash>]
151
- # @sg-ignore Declared return type Array<Hash> does not match inferred type Array,Hash,Array<Hash>
152
- # fixing cleanly needs restructuring the case/when to bind each branch to a typed local; leaving
153
- # for a follow-up code PR to keep this one annotation-only.
141
+ # @return [Array<Hash{"gid" => String} & Hash{"name" => String}>]
142
+ # rubocop:enable Layout/LineLength, YARD/TagTypeSyntax
154
143
  def resource_custom_field_enum_values(custom_field)
155
144
  resource_subtype = custom_field.fetch('resource_subtype')
156
145
  case resource_subtype
157
146
  when 'enum'
158
- # @type [Array<Hash>]
159
147
  [custom_field.fetch('enum_value')]
160
148
  when 'multi_enum'
161
- # @type [Array<Hash>]
162
149
  custom_field.fetch('multi_enum_values')
163
150
  else
164
151
  raise "Teach me how to handle resource_subtype #{resource_subtype}"
165
152
  end
166
153
  end
167
154
 
168
- # @param custom_field [Hash]
155
+ # @param custom_field [Hash{String => Object}]
169
156
  # @param enum_value [Hash{String => String}, nil]
170
157
  # @return [Array<String>]
171
158
  def find_gids(custom_field, enum_value)
@@ -69,16 +69,14 @@ module Checkoff
69
69
  #
70
70
  # @return [void]
71
71
  def enrich_webhook_subscription!(webhook_subscription)
72
- webhook_subscription&.fetch('filters', nil)&.map! do |filter|
72
+ return if webhook_subscription.nil?
73
+
74
+ webhook_subscription.fetch('filters', nil)&.map! do |filter|
73
75
  enrich_filter(filter)
74
76
  end
75
- resource = webhook_subscription&.fetch('resource', nil)
77
+ resource = webhook_subscription.fetch('resource', nil)
76
78
  name, resource_type = enrich_gid(resource) if resource
77
- # @sg-ignore webhook_subscription is declared [Hash, nil] but this line assumes non-nil
78
- # without the &. used elsewhere in this method — looks like a real nil-safety gap, not
79
- # just a typing gap; flagging for the follow-up code PR rather than silently widening
80
79
  webhook_subscription['checkoff:enriched:name'] = name if name
81
- # @sg-ignore same nil-safety gap as above
82
80
  webhook_subscription['checkoff:enriched:resource_type'] = resource_type if resource_type
83
81
  end
84
82
 
@@ -100,8 +98,8 @@ module Checkoff
100
98
  #
101
99
  # @return [void]
102
100
  def enrich_filter_parent_gid!(filter)
103
- parent_gid = T.cast(filter['checkoff:parent.gid'], T.nilable(String))
104
- return unless parent_gid
101
+ parent_gid = filter['checkoff:parent.gid']
102
+ return unless parent_gid.is_a?(String)
105
103
 
106
104
  name, resource_type = enrich_gid(parent_gid)
107
105
  filter['checkoff:enriched:parent.name'] = name if name
@@ -112,9 +110,9 @@ module Checkoff
112
110
  #
113
111
  # @return [void]
114
112
  def enrich_filter_resource!(filter)
115
- resource_gid = T.cast(filter['checkoff:resource.gid'], T.nilable(String))
113
+ resource_gid = filter['checkoff:resource.gid']
116
114
 
117
- return unless resource_gid
115
+ return unless resource_gid.is_a?(String)
118
116
 
119
117
  task = tasks.task_by_gid(resource_gid)
120
118
  task_name = task&.name
@@ -125,8 +123,8 @@ module Checkoff
125
123
  #
126
124
  # @return [void]
127
125
  def enrich_filter_section!(filter)
128
- section_gid = T.cast(filter['checkoff:fetched.section.gid'], T.nilable(String))
129
- return unless section_gid
126
+ section_gid = filter['checkoff:fetched.section.gid']
127
+ return unless section_gid.is_a?(String)
130
128
 
131
129
  section = sections.section_by_gid(section_gid)
132
130
  name = section&.name
@@ -157,7 +155,7 @@ module Checkoff
157
155
  # @return [void]
158
156
  def enrich_event_resource!(asana_event)
159
157
  # @type [Hash{String => String}]
160
- resource = T.cast(asana_event['resource'], T::Hash[String, String])
158
+ resource = asana_event['resource']
161
159
  # @type [String]
162
160
  resource_type = resource.fetch('resource_type')
163
161
 
@@ -52,7 +52,7 @@ module Checkoff
52
52
 
53
53
  private
54
54
 
55
- # @param filter [Hash]
55
+ # @param filter [Hash{String => String, Array<String>}]
56
56
  # @param asana_event [Hash]
57
57
  # @param failures [Array<String>]
58
58
  #
@@ -20,12 +20,10 @@ module Checkoff
20
20
 
21
21
  # @param key [Symbol]
22
22
  # @return [Object]
23
- # @sg-ignore return type could not be inferred — cascades from the ENV.fetch RBS gap below
24
23
  def [](key)
25
24
  config_value = @config[key]
26
25
  return config_value unless config_value.nil?
27
26
 
28
- # @sg-ignore Unresolved call to fetch on RBS::Unnamed::ENVClass, Class<ENV>
29
27
  ENV.fetch(envvar_name(key), nil)
30
28
  end
31
29
 
@@ -57,10 +57,8 @@ module Logging
57
57
  private
58
58
 
59
59
  # @return [Symbol]
60
- # @sg-ignore return type could not be inferred — cascades from the ENV.fetch RBS gap below
61
60
  def log_level
62
61
  # rubocop:disable Style/RedundantFetchBlock
63
- # @sg-ignore Unresolved call to fetch on RBS::Unnamed::ENVClass, Class<ENV>
64
62
  ENV.fetch('LOG_LEVEL') { 'INFO' }.downcase.to_sym
65
63
  # rubocop:enable Style/RedundantFetchBlock
66
64
  end
@@ -10,9 +10,10 @@ module Checkoff
10
10
  def initialize(_deps = {}); end
11
11
 
12
12
  # @param project_obj [Asana::Resources::Project]
13
- # @param project [String, Symbol] - :not_specified, :my_tasks
13
+ # @param project [String, :not_specified, :my_tasks, nil] - a String is a
14
+ # project name
14
15
  #
15
- # @return [Hash]
16
+ # @return [Hash{String => Object}]
16
17
  def project_to_h(project_obj, project: :not_specified)
17
18
  project = project_obj.name if project == :not_specified
18
19
  project_hash = { **project_obj.to_h, 'project' => project }
@@ -26,8 +27,8 @@ module Checkoff
26
27
  # @param project_hash [Hash]
27
28
  # @return [void]
28
29
  def unwrap_custom_fields(project_hash)
29
- # @type [Array<Hash>,nil]
30
- custom_fields = project_hash.fetch('custom_fields', nil)
30
+ # @type [Array<Hash{String => Object}>,nil]
31
+ custom_fields = project_hash['custom_fields']
31
32
 
32
33
  return if custom_fields.nil?
33
34
 
@@ -41,7 +41,7 @@ module Checkoff
41
41
  end.transform_values(&:to_h)
42
42
  end
43
43
 
44
- # @type [Hash{String => Class<CustomFieldVariant>}]
44
+ # @type [Hash{String => Class<CustomFieldVariant::CustomFieldVariant>}]
45
45
  VARIANTS = {
46
46
  'is' => CustomFieldVariant::Is,
47
47
  'no_value' => CustomFieldVariant::NoValue,
@@ -59,19 +59,15 @@ module Checkoff
59
59
  # @param gid [String]
60
60
  # @param single_custom_field_params [Hash{String => Array<String>}]
61
61
  # @return [Array(Hash{String => String}, Array<Symbol, Array>)]
62
- # @sg-ignore
63
62
  def convert_single_custom_field_params(gid, single_custom_field_params)
64
63
  variant_key = "custom_field_#{gid}.variant"
65
64
  variant = single_custom_field_params.fetch(variant_key)
66
65
  remaining_params = single_custom_field_params.reject { |k, _v| k == variant_key }
67
66
  raise "Teach me how to handle #{variant_key} = #{variant}" unless variant.length == 1
68
67
 
69
- # @sg-ignore
70
- # @type [Class<CustomFieldVariant>]
71
- # @sg-ignore
68
+ # @type [Class<CustomFieldVariant::CustomFieldVariant>, nil]
72
69
  variant_class = VARIANTS[variant[0]]
73
70
  # @type [Array(Hash{String => String}, Array<Symbol, Array>)]
74
- # @sg-ignore
75
71
  return variant_class.new(gid, remaining_params).convert unless variant_class.nil?
76
72
 
77
73
  raise "Teach me how to handle #{variant_key} = #{variant}"
@@ -79,10 +75,14 @@ module Checkoff
79
75
 
80
76
  # @param key [String]
81
77
  # @return [String]
82
- # @sg-ignore
83
78
  def gid_from_custom_field_key(key)
84
- # @sg-ignore
85
- key.split('_')[2].split('.')[0]
79
+ gid_and_suffix = key.split('_')[2]
80
+ raise "Unexpected custom field param key: #{key}" if gid_and_suffix.nil?
81
+
82
+ gid = gid_and_suffix.split('.')[0]
83
+ raise "Unexpected custom field param key: #{key}" if gid.nil?
84
+
85
+ gid
86
86
  end
87
87
 
88
88
  # @return [Hash{String => Array<String>}]
@@ -15,6 +15,13 @@ module Checkoff
15
15
  @remaining_params = T.let(remaining_params, T::Hash[String, T::Array[String]])
16
16
  end
17
17
 
18
+ # Implemented by every subclass.
19
+ #
20
+ # @return [Array(Hash{String => String}, Array<Symbol, Array>)]
21
+ def convert
22
+ raise NotImplementedError
23
+ end
24
+
18
25
  private
19
26
 
20
27
  # @return [String]
@@ -15,7 +15,6 @@ module Checkoff
15
15
  end
16
16
 
17
17
  # @return [Array(Hash{String => String}, Array<Symbol, Array>)]
18
- # @sg-ignore
19
18
  def convert
20
19
  return [{}, []] if date_url_params.empty?
21
20
 
@@ -30,11 +29,19 @@ module Checkoff
30
29
 
31
30
  raise "Teach me to handle these parameters: #{date_url_params.inspect}" unless date_url_params.empty?
32
31
 
33
- out
32
+ ensure_matched!(out)
34
33
  end
35
34
 
36
35
  private
37
36
 
37
+ # @param out [Array(Hash{String => String}, Array<Symbol, Array>), nil]
38
+ # @return [Array(Hash{String => String}, Array<Symbol, Array>)]
39
+ def ensure_matched!(out)
40
+ return out if out
41
+
42
+ raise "no date param matched a known prefix: #{date_url_params.inspect}"
43
+ end
44
+
38
45
  # @param prefix [String]
39
46
  # @return [Array(Hash{String => String}, Array<Symbol, Array>)]
40
47
  def convert_for_prefix(prefix)
@@ -129,7 +136,6 @@ module Checkoff
129
136
  end
130
137
 
131
138
  # @param param_key [String]
132
- # @sg-ignore
133
139
  # @return [String]
134
140
  def get_single_param(param_key)
135
141
  raise "Expected #{param_key} to have at least one value" unless date_url_params.key? param_key
@@ -138,7 +144,10 @@ module Checkoff
138
144
 
139
145
  raise "Expected #{param_key} to have one value" if value.length != 1
140
146
 
141
- value[0]
147
+ single_value = value[0]
148
+ raise "Expected #{param_key} to have a non-nil value" if single_value.nil?
149
+
150
+ single_value
142
151
  end
143
152
 
144
153
  # @param prefix [String]
@@ -24,10 +24,8 @@ module Checkoff
24
24
  # @param url [String]
25
25
  # @return [Array(Hash{String => String}, Array)]
26
26
  def convert_params(url)
27
- # @sg-ignore URI#query can be nil for a URL with no query string; CGI.parse expects
28
- # String looks like a real gap (unhandled no-query-string case), not just a typing
29
- # gap, flagging for the follow-up code PR rather than silently asserting non-nil
30
- url_params = CGI.parse(URI.parse(url).query)
27
+ # URI#query is nil for a URL with no query string; CGI.parse('') correctly yields {}
28
+ url_params = CGI.parse(URI.parse(url).query || '')
31
29
  custom_field_params, date_url_params, simple_url_params = partition_url_params(url_params)
32
30
  custom_field_args, custom_field_task_selector = convert_custom_field_params(custom_field_params)
33
31
  date_url_args, date_task_selector = convert_date_params(date_url_params)
@@ -61,11 +59,8 @@ module Checkoff
61
59
  # @return [Array(Hash{String => Array<String>}, Hash{String => Array<String>}, Hash{String => Array<String>})]
62
60
  def partition_url_params(url_params)
63
61
  groups = T.let(url_params.to_a.group_by do |key, _values|
64
- # @sg-ignore Unresolved call to start_with? — T.let wrapping this
65
- # chained group_by block seems to block block-param inference
66
62
  if key.start_with? 'custom_field_'
67
63
  :custom_field
68
- # @sg-ignore Unresolved call to include? — same T.let/block-param gap
69
64
  elsif key.include? '_date'
70
65
  :date
71
66
  else
@@ -31,6 +31,15 @@ module Checkoff
31
31
  end
32
32
  end
33
33
 
34
+ public
35
+
36
+ # @return [Array<String>]
37
+ def convert
38
+ raise 'Implement me!'
39
+ end
40
+
41
+ private
42
+
34
43
  # @return [String]
35
44
  attr_reader :key
36
45
 
@@ -226,14 +235,13 @@ module Checkoff
226
235
  # @return [Hash{String => String}] the converted params
227
236
  def convert
228
237
  # @type [Array<Array(String, String)>]
229
- # @sg-ignore
230
238
  arr_of_tuples = simple_url_params.to_a.flat_map do |key, values|
231
239
  # @type
232
240
  entry = convert_arg(key, values).each_slice(2).to_a
233
241
  entry
234
242
  end
235
243
  # @type [Hash{String => String}]
236
- out = T.cast(arr_of_tuples.to_h, T::Hash[String, String])
244
+ out = arr_of_tuples.to_h
237
245
  unless out.include? 'sort_by'
238
246
  # keep results consistent between calls; API using default
239
247
  # sort_by does not seem to be.
@@ -265,14 +273,12 @@ module Checkoff
265
273
  # https://developers.asana.com/docs/search-tasks-in-a-workspace
266
274
  # @param key [String] the name of the search url param
267
275
  # @param values [Array<String>] the values of the search url param
268
- # @sg-ignore
269
- # @return [Hash{String => String}] the converted params
276
+ # @return [Array<String>] the converted params, as an alternating key/value flat array
270
277
  def convert_arg(key, values)
271
278
  # @type [Class<SimpleParam::SimpleParam>]
272
279
  clazz = ARGS.fetch(key)
273
280
  # @type [SimpleParam::SimpleParam]
274
281
  obj = clazz.new(key:, values:)
275
- # @sg-ignore
276
282
  obj.convert
277
283
  end
278
284
 
@@ -53,7 +53,7 @@ module Checkoff
53
53
  { sections:, projects:, client:, custom_fields: }
54
54
  end
55
55
 
56
- # @return [Asana::Resources::Project]
56
+ # @return [Asana::Resources::Section]
57
57
  attr_reader :item
58
58
  # @return [Checkoff::Sections]
59
59
  attr_reader :sections
@@ -8,7 +8,7 @@ module Checkoff
8
8
  module Common
9
9
  # Base class to evaluate a project selector function given fully evaluated arguments
10
10
  class FunctionEvaluator < ::Checkoff::SelectorClasses::FunctionEvaluator
11
- # @param selector [Array(Symbol, Array), String]
11
+ # @param selector [Array<Symbol, Array>, Symbol]
12
12
  # @param custom_fields [Checkoff::CustomFields]
13
13
  # @param _kwargs [Hash]
14
14
  def initialize(selector:, custom_fields:, **_kwargs)
@@ -17,9 +17,12 @@ module Checkoff
17
17
  super()
18
18
  end
19
19
 
20
+ # @return [Checkoff::CustomFields]
21
+ attr_reader :custom_fields
22
+
20
23
  private
21
24
 
22
- # @return [Array(Symbol, Array), String]
25
+ # @return [Array<Symbol, Array>, Symbol]
23
26
  attr_reader :selector
24
27
  end
25
28
  end