checkoff 0.19.2 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b199a642ccf75e4369caeffa5fc9cd5248164dc76f23674af38ce77dc2dfb3a
4
- data.tar.gz: 70a1db49e52e64294d44b9e1b386464b5e9a9c8cf28497fab4493abc77932954
3
+ metadata.gz: d7c237ce35c02738d3170942d13d3d47d02d0fc0ed65eb78b836b7c444aed241
4
+ data.tar.gz: 8ab5d779ab9feadeeb66d12294e25f8b2baf0de32d8a917618f3adc26c77a4ea
5
5
  SHA512:
6
- metadata.gz: '0088414917271dba3b10842d25cfe151188325582fd2fd07e7832f0ab849ccb6058c342d7b3c8fc754f54ce52f639db791562f549d509b2ecd5701b2dfd52e41'
7
- data.tar.gz: b91b8209aa5b765723a742cf86e0490935667bfa06a0b16fd5a7269dd0a743d84608da35b2a8510951db4180a50ad252d4ea39c2130a98013d6e8bf160db4f93
6
+ metadata.gz: '078c32912450408243aea106d973047fabfb4db52c6267f2dd0030894a7f1b898a89acce8115da066e2b1fa2e45560286148a7aa6ccc0632d869797cf4b008aa'
7
+ data.tar.gz: 388f84ed37bc63dc68182e4b484e18fc5ab591625840c9f6a4f60fa385340cf6e7c4386cd56ee146100d3d5e99952f8d7c77e9c12c3ae9e47fbabc6b7e9e773e
data/.circleci/config.yml CHANGED
@@ -37,9 +37,9 @@ commands:
37
37
  sed -E -e 's/checkoff \([[:digit:]]+.[[:digit:]]+.[[:digit:]]+\)/checkoff (0.1.0)/g' \
38
38
  Gemfile.lock > Gemfile.lock.deversioned
39
39
  - restore_cache:
40
- key: gems-v1-{{ checksum "Gemfile.lock.deversioned" }}
40
+ key: gems-v2-{{ checksum "Gemfile.lock.deversioned" }}
41
41
  - restore_cache:
42
- key: wheels-v1-{{ checksum "requirements_dev.txt" }}
42
+ key: wheels-v2-{{ checksum "requirements_dev.txt" }}
43
43
  - run:
44
44
  name: Initialize packages
45
45
  command: |
@@ -54,11 +54,11 @@ commands:
54
54
  exit 1
55
55
  fi
56
56
  - save_cache:
57
- key: gems-v1-{{ checksum "Gemfile.lock.deversioned" }}
57
+ key: gems-v2-{{ checksum "Gemfile.lock.deversioned" }}
58
58
  paths:
59
59
  - "vendor/bundle"
60
60
  - save_cache:
61
- key: wheels-v1-{{ checksum "requirements_dev.txt" }}
61
+ key: wheels-v2-{{ checksum "requirements_dev.txt" }}
62
62
  paths:
63
63
  - "/home/circleci/.cache/pip/wheels"
64
64
  - run:
@@ -104,13 +104,8 @@ jobs:
104
104
  steps:
105
105
  - set_up_environment
106
106
  - run_with_languages:
107
- label: Test
108
- command: |
109
- bundle exec rake citest
110
- # https://github.com/bluelabsio/records-mover/blob/master/Makefile#L25
111
- git status --porcelain coverage/.last_run.json
112
- git diff coverage/.last_run.json
113
- test -z "$(git status --porcelain coverage/.last_run.json)"
107
+ label: test
108
+ command: make citest cicoverage
114
109
  # This seemed to shave 5ish% of the build time off when added
115
110
  resource_class: large
116
111
 
data/.gitignore CHANGED
@@ -57,8 +57,4 @@ requirements_dev.txt.installed
57
57
  /pkg/
58
58
  /spec/reports/
59
59
  /tmp/
60
- /coverage/.resultset.json
61
- /coverage/.resultset.json.lock
62
- /coverage/assets/
63
- /coverage/index.html
64
- /coverage/lcov/
60
+ /coverage/
data/.overcommit.yml CHANGED
@@ -17,6 +17,15 @@
17
17
  # Uncomment the following lines to make the configuration take effect.
18
18
 
19
19
  PreCommit:
20
+ # Extend default config at https://github.com/sds/overcommit/blob/master/config/default.yml
21
+ BrokenSymlinks:
22
+ enabled: true
23
+ description: 'Check for broken symlinks'
24
+ quiet: true
25
+ exclude:
26
+ # This is a symlink to sibling checkout of vincelifedaily, used
27
+ # only in local development
28
+ - config/env.1p
20
29
  RuboCop:
21
30
  enabled: true
22
31
  on_warn: fail # Treat all warnings as failures
@@ -29,6 +38,10 @@ PreCommit:
29
38
  - '**/Gemfile'
30
39
  - '**/Rakefile'
31
40
  - 'bin/*'
41
+ - 'script/*'
42
+ exclude:
43
+ - db/migrate/*.rb
44
+ - db/schema.rb
32
45
  PythonFlake8:
33
46
  enabled: true
34
47
  on_warn: fail
@@ -37,6 +50,7 @@ PreCommit:
37
50
  on_warn: fail
38
51
  include:
39
52
  - '.envrc'
53
+ - '**/*.sh'
40
54
  YamlLint:
41
55
  enabled: true
42
56
  on_warn: fail
@@ -53,3 +67,10 @@ PrePush:
53
67
  include:
54
68
  - 'test/unit/**/test*.rb'
55
69
  enabled: true
70
+
71
+ #PostCheckout:
72
+ # ALL: # Special hook name that customizes all hooks of this type
73
+ # quiet: true # Change all post-checkout hooks to only display output on failure
74
+ #
75
+ # IndexTags:
76
+ # enabled: true # Generate a tags file with `ctags` each time HEAD changes
data/.rubocop.yml CHANGED
@@ -98,7 +98,7 @@ Style/TrivialAccessors:
98
98
 
99
99
  AllCops:
100
100
  NewCops: enable
101
- TargetRubyVersion: 2.6
101
+ TargetRubyVersion: 2.7
102
102
  Exclude:
103
103
  - 'bin/*'
104
104
  - 'vendor/**/*'
data/Gemfile.lock CHANGED
@@ -12,7 +12,7 @@ GIT
12
12
  PATH
13
13
  remote: .
14
14
  specs:
15
- checkoff (0.19.2)
15
+ checkoff (0.21.0)
16
16
  activesupport
17
17
  asana (> 0.10.0)
18
18
  cache_method
@@ -22,13 +22,16 @@ PATH
22
22
  GEM
23
23
  remote: https://rubygems.org/
24
24
  specs:
25
- activesupport (6.1.6.1)
25
+ activesupport (7.0.4)
26
26
  concurrent-ruby (~> 1.0, >= 1.0.2)
27
27
  i18n (>= 1.6, < 2)
28
28
  minitest (>= 5.1)
29
29
  tzinfo (~> 2.0)
30
- zeitwerk (~> 2.3)
30
+ addressable (2.8.1)
31
+ public_suffix (>= 2.0.2, < 6.0)
32
+ ansi (1.5.0)
31
33
  ast (2.4.2)
34
+ builder (3.2.4)
32
35
  bump (0.10.0)
33
36
  cache (0.4.1)
34
37
  cache_method (0.2.7)
@@ -38,8 +41,12 @@ GEM
38
41
  childprocess (4.0.0)
39
42
  coderay (1.1.3)
40
43
  concurrent-ruby (1.1.10)
41
- dalli (3.2.2)
44
+ crack (0.4.5)
45
+ rexml
46
+ dalli (3.2.3)
47
+ diff-lcs (1.5.0)
42
48
  docile (1.4.0)
49
+ fakeweb (1.3.0)
43
50
  faraday (1.5.1)
44
51
  faraday-em_http (~> 1.0)
45
52
  faraday-em_synchrony (~> 1.0)
@@ -63,17 +70,19 @@ GEM
63
70
  faraday_middleware
64
71
  multi_json
65
72
  gli (2.21.0)
73
+ hashdiff (1.0.1)
66
74
  i18n (1.12.0)
67
75
  concurrent-ruby (~> 1.0)
68
76
  imagen (0.1.8)
69
77
  parser (>= 2.5, != 2.5.1.1)
70
78
  iniparse (1.5.0)
79
+ json (2.6.2)
71
80
  jwt (2.2.3)
72
81
  kramdown (2.4.0)
73
82
  rexml
74
83
  kramdown-parser-gfm (1.1.0)
75
84
  kramdown (~> 2.0)
76
- mdl (0.11.0)
85
+ mdl (0.12.0)
77
86
  kramdown (~> 2.3)
78
87
  kramdown-parser-gfm (~> 1.1)
79
88
  mixlib-cli (~> 2.1, >= 2.1.1)
@@ -82,12 +91,17 @@ GEM
82
91
  method_source (1.0.0)
83
92
  minitest (5.16.2)
84
93
  minitest-profile (0.0.2)
94
+ minitest-reporters (1.5.0)
95
+ ansi
96
+ builder
97
+ minitest (>= 5.0)
98
+ ruby-progressbar
85
99
  mixlib-cli (2.1.8)
86
100
  mixlib-config (3.0.27)
87
101
  tomlrb
88
102
  mixlib-shellout (3.2.7)
89
103
  chef-utils
90
- mocha (1.12.0)
104
+ mocha (2.0.0.alpha.1)
91
105
  multi_json (1.15.0)
92
106
  multi_xml (0.6.0)
93
107
  multipart-post (2.1.1)
@@ -101,35 +115,50 @@ GEM
101
115
  childprocess (>= 0.6.3, < 5)
102
116
  iniparse (~> 1.4)
103
117
  rexml (~> 3.2)
104
- parallel (1.20.1)
105
- parser (3.0.2.0)
118
+ parallel (1.22.1)
119
+ parser (3.1.2.1)
106
120
  ast (~> 2.4.1)
107
121
  pry (0.14.1)
108
122
  coderay (~> 1.1)
109
123
  method_source (~> 1.0)
124
+ public_suffix (5.0.0)
110
125
  rack (2.2.4)
111
- rainbow (3.0.0)
126
+ rainbow (3.1.1)
112
127
  rake (13.0.3)
113
- regexp_parser (2.1.1)
128
+ regexp_parser (2.6.0)
114
129
  rexml (3.2.5)
115
- rubocop (1.18.4)
130
+ rspec (3.11.0)
131
+ rspec-core (~> 3.11.0)
132
+ rspec-expectations (~> 3.11.0)
133
+ rspec-mocks (~> 3.11.0)
134
+ rspec-core (3.11.0)
135
+ rspec-support (~> 3.11.0)
136
+ rspec-expectations (3.11.1)
137
+ diff-lcs (>= 1.2.0, < 2.0)
138
+ rspec-support (~> 3.11.0)
139
+ rspec-mocks (3.11.1)
140
+ diff-lcs (>= 1.2.0, < 2.0)
141
+ rspec-support (~> 3.11.0)
142
+ rspec-support (3.11.1)
143
+ rubocop (1.36.0)
144
+ json (~> 2.3)
116
145
  parallel (~> 1.10)
117
- parser (>= 3.0.0.0)
146
+ parser (>= 3.1.2.1)
118
147
  rainbow (>= 2.2.2, < 4.0)
119
148
  regexp_parser (>= 1.8, < 3.0)
120
- rexml
121
- rubocop-ast (>= 1.8.0, < 2.0)
149
+ rexml (>= 3.2.5, < 4.0)
150
+ rubocop-ast (>= 1.20.1, < 2.0)
122
151
  ruby-progressbar (~> 1.7)
123
152
  unicode-display_width (>= 1.4.0, < 3.0)
124
- rubocop-ast (1.8.0)
125
- parser (>= 3.0.1.1)
153
+ rubocop-ast (1.22.0)
154
+ parser (>= 3.1.1.0)
126
155
  rubocop-minitest (0.12.1)
127
156
  rubocop (>= 0.90, < 2.0)
128
157
  rubocop-rake (0.5.1)
129
158
  rubocop
130
159
  ruby-progressbar (1.11.0)
131
160
  ruby2_keywords (0.0.5)
132
- rugged (1.1.0)
161
+ rugged (1.5.0.1)
133
162
  simplecov (0.21.2)
134
163
  docile (~> 1.1)
135
164
  simplecov-html (~> 0.11)
@@ -140,17 +169,21 @@ GEM
140
169
  tomlrb (2.0.3)
141
170
  tzinfo (2.0.5)
142
171
  concurrent-ruby (~> 1.0)
143
- undercover (0.4.3)
172
+ undercover (0.4.5)
144
173
  imagen (>= 0.1.8)
145
174
  rainbow (>= 2.1, < 4.0)
146
- rugged (>= 0.27, < 1.2)
147
- unicode-display_width (2.0.0)
148
- zeitwerk (2.6.0)
175
+ rugged (>= 0.27, < 1.6)
176
+ unicode-display_width (2.3.0)
177
+ webmock (3.18.1)
178
+ addressable (>= 2.8.0)
179
+ crack (>= 0.3.2)
180
+ hashdiff (>= 0.4.0, < 2.0.0)
149
181
 
150
182
  PLATFORMS
151
183
  ruby
152
184
  x86_64-darwin-19
153
185
  x86_64-darwin-20
186
+ x86_64-darwin-21
154
187
  x86_64-linux
155
188
 
156
189
  DEPENDENCIES
@@ -158,18 +191,22 @@ DEPENDENCIES
158
191
  bump
159
192
  bundler
160
193
  checkoff!
194
+ fakeweb
161
195
  mdl
162
196
  minitest-profile
163
- mocha
197
+ minitest-reporters
198
+ mocha (~> 2.0.0.alpha.1)
164
199
  overcommit (>= 0.58.0)
165
200
  pry
166
201
  rake (~> 13.0)
167
- rubocop
202
+ rspec (>= 3.4)
203
+ rubocop (~> 1.36)
168
204
  rubocop-minitest
169
205
  rubocop-rake
170
206
  simplecov (>= 0.18.0)
171
207
  simplecov-lcov
172
208
  undercover
209
+ webmock
173
210
 
174
211
  BUNDLED WITH
175
- 2.2.25
212
+ 2.3.26
data/Makefile CHANGED
@@ -15,7 +15,17 @@ export PRINT_HELP_PYSCRIPT
15
15
  help:
16
16
  @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
17
17
 
18
- default: localtest ## run default tests and quality
18
+ default: clean-coverage test coverage clean-typecoverage typecheck typecoverage quality ## run default typechecking, tests and quality
19
+
20
+ typecheck: ## validate types in code and configuration
21
+
22
+ citypecheck: typecheck ## Run type check from CircleCI
23
+
24
+ typecoverage: typecheck ## Run type checking and then ratchet coverage in metrics/
25
+
26
+ clean-typecoverage: ## Clean out type-related coverage previous results to avoid flaky results
27
+
28
+ citypecoverage: typecoverage ## Run type checking, ratchet coverage, and then complain if ratchet needs to be committed
19
29
 
20
30
  requirements_dev.txt.installed: requirements_dev.txt
21
31
  pip install -q --disable-pip-version-check -r requirements_dev.txt
@@ -40,7 +50,7 @@ clear_metrics: ## remove or reset result artifacts created by tests and quality
40
50
  clean: clear_metrics ## remove all built artifacts
41
51
 
42
52
 
43
- typecheck: ## validate types in code and configuration
53
+ citest: test ## Run unit tests from CircleCI
44
54
 
45
55
  overcommit: ## run precommit quality checks
46
56
  bundle exec overcommit --run
@@ -56,6 +66,16 @@ localtest: ## run default local actions
56
66
  repl: ## Load up checkoff in pry
57
67
  @bundle exec rake repl
58
68
 
69
+ clean-coverage:
70
+ @bundle exec rake clear_metrics
71
+
72
+ coverage: test report-coverage ## check code coverage
73
+ @bundle exec rake undercover
74
+
75
+ report-coverage: test ## Report summary of coverage to stdout, and generate HTML, XML coverage report
76
+
77
+ cicoverage: coverage ## check code coverage
78
+
59
79
  update_from_cookiecutter: ## Bring in changes from template project used to create this repo
60
80
  bundle exec overcommit --uninstall
61
81
  IN_COOKIECUTTER_PROJECT_UPGRADER=1 cookiecutter_project_upgrader || true
data/checkoff.gemspec CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
13
13
  spec.summary = 'Command-line and gem client for Asana (unofficial)'
14
14
  spec.homepage = 'https://github.com/apiology/checkoff'
15
15
  spec.license = 'MIT license'
16
- spec.required_ruby_version = '>= 2.6'
16
+ spec.required_ruby_version = '>= 2.7'
17
17
 
18
18
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
19
19
  `git ls-files -z`.split("\x0").reject do |f|
@@ -32,20 +32,27 @@ Gem::Specification.new do |spec|
32
32
 
33
33
  spec.add_development_dependency 'bump'
34
34
  spec.add_development_dependency 'bundler'
35
+ spec.add_development_dependency 'fakeweb'
35
36
  spec.add_development_dependency 'mdl'
36
37
  spec.add_development_dependency 'minitest-profile'
37
- spec.add_development_dependency 'mocha'
38
+ spec.add_development_dependency 'minitest-reporters'
39
+ spec.add_development_dependency 'mocha', ['~> 2.0.0.alpha.1']
38
40
  # 0.58.0 and 0.57.0 don't seem super compatible with signatures, and
39
41
  # magit doesn't seem to want to use the bundled version at the moment,
40
42
  # so let's favor the more recent version...
41
43
  spec.add_development_dependency 'overcommit', ['>=0.58.0']
42
44
  spec.add_development_dependency 'pry'
43
45
  spec.add_development_dependency 'rake', '~> 13.0'
44
- spec.add_development_dependency 'rubocop'
46
+ spec.add_development_dependency 'rspec', '>=3.4'
47
+ spec.add_development_dependency 'rubocop', ['~> 1.36']
45
48
  spec.add_development_dependency 'rubocop-minitest'
46
49
  spec.add_development_dependency 'rubocop-rake'
47
50
  # ensure version with branch coverage
48
51
  spec.add_development_dependency 'simplecov', ['>=0.18.0']
49
52
  spec.add_development_dependency 'simplecov-lcov'
50
53
  spec.add_development_dependency 'undercover'
54
+ spec.add_development_dependency 'webmock'
55
+ spec.metadata = {
56
+ 'rubygems_mfa_required' => 'true',
57
+ }
51
58
  end
data/fix.sh CHANGED
@@ -69,7 +69,18 @@ ensure_rbenv() {
69
69
 
70
70
  latest_ruby_version() {
71
71
  major_minor=${1}
72
- rbenv install --list 2>/dev/null | grep "^${major_minor}."
72
+
73
+ # Double check that this command doesn't error out under -e
74
+ rbenv install --list >/dev/null 2>&1
75
+
76
+ # not sure why, but 'rbenv install --list' below exits with error code
77
+ # 1...after providing the same output the previous line gave when it
78
+ # exited with error code 0.
79
+ #
80
+ # https://github.com/rbenv/rbenv/issues/1441
81
+ set +e
82
+ rbenv install --list 2>/dev/null | cat | grep "^${major_minor}."
83
+ set -e
73
84
  }
74
85
 
75
86
  ensure_dev_library() {
@@ -91,12 +102,20 @@ ensure_ruby_build_requirements() {
91
102
  ensure_dev_library openssl/ssl.h openssl libssl-dev
92
103
  }
93
104
 
105
+ ensure_latest_ruby_build_definitions() {
106
+ ensure_rbenv
107
+
108
+ git -C "$(rbenv root)"/plugins/ruby-build pull
109
+ }
110
+
94
111
  # You can find out which feature versions are still supported / have
95
112
  # been release here: https://www.ruby-lang.org/en/downloads/
96
113
  ensure_ruby_versions() {
114
+ ensure_latest_ruby_build_definitions
115
+
97
116
  # You can find out which feature versions are still supported / have
98
117
  # been release here: https://www.ruby-lang.org/en/downloads/
99
- ruby_versions="$(latest_ruby_version 2.6)"
118
+ ruby_versions="$(latest_ruby_version 2.7)"
100
119
 
101
120
  echo "Latest Ruby versions: ${ruby_versions}"
102
121
 
@@ -169,7 +188,7 @@ ensure_bundle() {
169
188
  #
170
189
  # This affects nokogiri, which will try to reinstall itself in
171
190
  # Docker builds where it's already installed if this is not run.
172
- for platform in x86_64-darwin-20 x86_64-linux
191
+ for platform in x86_64-darwin-21 x86_64-linux
173
192
  do
174
193
  grep "${platform:?}" Gemfile.lock >/dev/null 2>&1 || bundle lock --add-platform "${platform:?}"
175
194
  done
@@ -18,7 +18,7 @@ module Checkoff
18
18
  config_value = @config[key]
19
19
  return config_value unless config_value.nil?
20
20
 
21
- ENV[envvar_name(key)]
21
+ ENV.fetch(envvar_name(key), nil)
22
22
  end
23
23
 
24
24
  def fetch(key)
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'custom_field_variant'
4
+
5
+ module Checkoff
6
+ module Internal
7
+ module SearchUrl
8
+ # Convert custom field parameters from an Asana search URL into
9
+ # API search arguments and Checkoff task selectors
10
+ class CustomFieldParamConverter
11
+ def initialize(custom_field_params:)
12
+ @custom_field_params = custom_field_params
13
+ end
14
+
15
+ def convert
16
+ args = {}
17
+ task_selector = []
18
+ by_custom_field.each do |gid, single_custom_field_params|
19
+ new_args, new_task_selector = convert_single_custom_field_params(gid,
20
+ single_custom_field_params)
21
+ args, task_selector = merge_args_and_task_selectors(args, new_args,
22
+ task_selector, new_task_selector)
23
+ end
24
+ [args, task_selector]
25
+ end
26
+
27
+ private
28
+
29
+ def by_custom_field
30
+ custom_field_params.group_by do |key, _value|
31
+ gid_from_custom_field_key(key)
32
+ end.transform_values(&:to_h)
33
+ end
34
+
35
+ def merge_args_and_task_selectors(args, new_args, task_selector, new_task_selector)
36
+ args = args.merge(new_args)
37
+ return [args, task_selector] if new_task_selector == []
38
+
39
+ raise 'Teach me how to merge task selectors' unless task_selector == []
40
+
41
+ task_selector = new_task_selector
42
+
43
+ [args, task_selector]
44
+ end
45
+
46
+ VARIANTS = {
47
+ 'is' => CustomFieldVariant::Is,
48
+ 'no_value' => CustomFieldVariant::NoValue,
49
+ 'is_not' => CustomFieldVariant::IsNot,
50
+ 'less_than' => CustomFieldVariant::LessThan,
51
+ }.freeze
52
+
53
+ def convert_single_custom_field_params(gid, single_custom_field_params)
54
+ variant_key = "custom_field_#{gid}.variant"
55
+ variant = single_custom_field_params.fetch(variant_key)
56
+ remaining_params = single_custom_field_params.reject { |k, _v| k == variant_key }
57
+ raise "Teach me how to handle #{variant_key} = #{variant}" unless variant.length == 1
58
+
59
+ variant_class = VARIANTS[variant[0]]
60
+ return variant_class.new(gid, remaining_params).convert unless variant_class.nil?
61
+
62
+ raise "Teach me how to handle #{variant_key} = #{variant}"
63
+ end
64
+
65
+ def gid_from_custom_field_key(key)
66
+ key.split('_')[2].split('.')[0]
67
+ end
68
+
69
+ attr_reader :custom_field_params
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkoff
4
+ module Internal
5
+ module SearchUrl
6
+ module CustomFieldVariant
7
+ # base class for handling different custom_field_#{gid}.variant params
8
+ class CustomFieldVariant
9
+ def initialize(gid, remaining_params)
10
+ @gid = gid
11
+ @remaining_params = remaining_params
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :gid, :remaining_params
17
+ end
18
+
19
+ # custom_field_#{gid}.variant = 'less_than'
20
+ class LessThan < CustomFieldVariant
21
+ def convert
22
+ max_param = "custom_field_#{gid}.max"
23
+ case remaining_params.keys
24
+ when [max_param]
25
+ convert_single_custom_field_less_than_params_max_param(max_param)
26
+ else
27
+ raise "Teach me how to handle #{remaining_params}"
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def convert_single_custom_field_less_than_params_max_param(max_param)
34
+ max_values = remaining_params.fetch(max_param)
35
+ unless max_values.length == 1
36
+ raise "Teach me how to handle these remaining keys for #{max_param}: #{remaining_params}"
37
+ end
38
+
39
+ max_value = max_values[0]
40
+ empty_task_selector = []
41
+ [{ "custom_fields.#{gid}.less_than" => max_value }, empty_task_selector]
42
+ end
43
+ end
44
+
45
+ # custom_field_#{gid}.variant = 'is_not'
46
+ class IsNot < CustomFieldVariant
47
+ def convert
48
+ case remaining_params.keys
49
+ when ["custom_field_#{gid}.selected_options"]
50
+ convert_single_custom_field_is_not_params_selected_options
51
+ else
52
+ raise "Teach me how to handle #{remaining_params}"
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def convert_single_custom_field_is_not_params_selected_options
59
+ selected_options = remaining_params.fetch("custom_field_#{gid}.selected_options")
60
+ raise "Teach me how to handle #{remaining_params}" unless selected_options.length == 1
61
+
62
+ [{ "custom_fields.#{gid}.is_set" => 'true' },
63
+ ['not',
64
+ ['custom_field_gid_value_contains_any_gid',
65
+ gid,
66
+ selected_options.fetch(0).split('~')]]]
67
+ end
68
+ end
69
+
70
+ # custom_field_#{gid}.variant = 'no_value'
71
+ class NoValue < CustomFieldVariant
72
+ def convert
73
+ unless remaining_params.length.zero?
74
+ raise "Teach me how to handle these remaining keys for #{variant_key}: #{remaining_params}"
75
+ end
76
+
77
+ empty_task_selector = []
78
+ [{ "custom_fields.#{gid}.is_set" => 'false' }, empty_task_selector]
79
+ end
80
+ end
81
+
82
+ # custom_field_#{gid}.variant = 'is'
83
+ class Is < CustomFieldVariant
84
+ def convert
85
+ unless remaining_params.length == 1
86
+ raise "Teach me how to handle these remaining keys for #{variant_key}: #{remaining_params}"
87
+ end
88
+
89
+ key, values = remaining_params.to_a[0]
90
+ convert_custom_field_is_arg(key, values)
91
+ end
92
+
93
+ private
94
+
95
+ def convert_custom_field_is_arg(key, values)
96
+ empty_task_selector = []
97
+
98
+ if key.end_with? '.selected_options'
99
+ raise "Too many values found for #{key}: #{values}" if values.length != 1
100
+
101
+ return [{ "custom_fields.#{gid}.value" => values[0] },
102
+ empty_task_selector]
103
+ end
104
+
105
+ raise "Teach me how to handle #{key} = #{values}"
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'simple_param_converter'
6
+ require_relative 'custom_field_param_converter'
7
+
8
+ module Checkoff
9
+ module Internal
10
+ module SearchUrl
11
+ # Parse Asana search URLs into parameters suitable to pass into
12
+ # the /workspaces/{workspace_gid}/tasks/search endpoint
13
+ class Parser
14
+ def initialize(_deps = {})
15
+ # allow dependencies to be passed in by tests
16
+ end
17
+
18
+ def convert_params(url)
19
+ url_params = CGI.parse(URI.parse(url).query)
20
+ custom_field_params, simple_url_params = partition_url_params(url_params)
21
+ custom_field_args, task_selector = convert_custom_field_params(custom_field_params)
22
+ simple_url_args = convert_simple_params(simple_url_params)
23
+ [custom_field_args.merge(simple_url_args), task_selector]
24
+ end
25
+
26
+ private
27
+
28
+ def convert_simple_params(simple_url_params)
29
+ SimpleParamConverter.new(simple_url_params: simple_url_params).convert
30
+ end
31
+
32
+ def convert_custom_field_params(custom_field_params)
33
+ CustomFieldParamConverter.new(custom_field_params: custom_field_params).convert
34
+ end
35
+
36
+ def partition_url_params(url_params)
37
+ url_params.to_a.partition do |key, _values|
38
+ key.start_with? 'custom_field_'
39
+ end.map(&:to_h)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'custom_field_param_converter'
4
+
5
+ module Checkoff
6
+ module Internal
7
+ module SearchUrl
8
+ module SimpleParam
9
+ # base class for handling different types of search url params
10
+ class SimpleParam
11
+ def initialize(values:)
12
+ @values = values
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :values
18
+ end
19
+
20
+ # Handle 'any_projects.ids' search url param
21
+ class AnyProjectsIds < SimpleParam
22
+ def convert
23
+ ['projects.any', values.join(',')]
24
+ end
25
+ end
26
+
27
+ # Handle 'completion' search url param
28
+ class Completion < SimpleParam
29
+ def convert
30
+ raise "Teach me how to handle #{key} = #{values}" if values.length != 1
31
+
32
+ value = values.fetch(0)
33
+ raise "Teach me how to handle #{key} = #{values}" if value != 'incomplete'
34
+
35
+ ['completed', false]
36
+ end
37
+ end
38
+
39
+ # Handle 'not_tags.ids' search url param
40
+ class NotTagsIds < SimpleParam
41
+ def convert
42
+ raise "Teach me how to handle #{key} = #{values}" if values.length != 1
43
+
44
+ value = values.fetch(0)
45
+ tag_ids = value.split('~')
46
+ ['tags.not', tag_ids.join(',')]
47
+ end
48
+ end
49
+ end
50
+
51
+ # Convert simple parameters - ones where the param name itself
52
+ # doesn't encode any parameters'
53
+ class SimpleParamConverter
54
+ def initialize(simple_url_params:)
55
+ @simple_url_params = simple_url_params
56
+ end
57
+
58
+ def convert
59
+ simple_url_params.to_a.to_h do |key, values|
60
+ convert_arg(key, values)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ ARGS = {
67
+ 'any_projects.ids' => SimpleParam::AnyProjectsIds,
68
+ 'completion' => SimpleParam::Completion,
69
+ 'not_tags.ids' => SimpleParam::NotTagsIds,
70
+ }.freeze
71
+
72
+ # https://developers.asana.com/docs/search-tasks-in-a-workspace
73
+ def convert_arg(key, values)
74
+ clazz = ARGS.fetch(key)
75
+ clazz.new(values: values).convert
76
+ end
77
+
78
+ attr_reader :simple_url_params
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'search_url/parser'
@@ -110,7 +110,8 @@ module Checkoff
110
110
  raise "Could not find task in project_gid #{project_gid}: #{task}" if membership.nil?
111
111
 
112
112
  current_section = membership['section']['name']
113
- current_section = nil if ['(no section)', 'Untitled section'].include?(current_section)
113
+ inbox_section_names = ['(no section)', 'Untitled section', 'Inbox']
114
+ current_section = nil if inbox_section_names.include?(current_section)
114
115
  by_section[current_section] ||= []
115
116
  by_section[current_section] << task
116
117
  end
@@ -41,7 +41,7 @@ module Checkoff
41
41
  def raw_subtasks(task)
42
42
  task_options = projects.task_options
43
43
  task_options[:options][:fields] << 'is_rendered_as_separator'
44
- task.subtasks(task_options)
44
+ task.subtasks(**task_options)
45
45
  end
46
46
  cache_method :raw_subtasks, SHORT_CACHE_TIME
47
47
 
@@ -12,7 +12,7 @@ require_relative 'task_selectors'
12
12
  require 'asana/resource_includes/collection'
13
13
  require 'asana/resource_includes/response_helper'
14
14
 
15
- require 'checkoff/internal/search_url_parser'
15
+ require 'checkoff/internal/search_url'
16
16
 
17
17
  # https://developers.asana.com/docs/task-searches
18
18
  module Checkoff
@@ -33,7 +33,7 @@ module Checkoff
33
33
  projects: Checkoff::Projects.new(config: config),
34
34
  clients: Checkoff::Clients.new(config: config),
35
35
  client: clients.client,
36
- search_url_parser: Checkoff::Internal::SearchUrlParser.new,
36
+ search_url_parser: Checkoff::Internal::SearchUrl::Parser.new,
37
37
  asana_resources_collection_class: Asana::Resources::Collection)
38
38
  @workspaces = workspaces
39
39
  @task_selectors = task_selectors
@@ -3,5 +3,5 @@
3
3
  # Command-line and gem client for Asana (unofficial)
4
4
  module Checkoff
5
5
  # Version of library
6
- VERSION = '0.19.2'
6
+ VERSION = '0.21.0'
7
7
  end
@@ -3,15 +3,7 @@
3
3
  desc 'Ensure that any locally ratcheted coverage metrics are cleared back ' \
4
4
  'to git baseline'
5
5
  task :clear_metrics do |_t|
6
- ret =
7
- system('git checkout coverage/.last_run.json')
8
- raise unless ret
9
-
10
6
  # Without this old lines which are removed are still counted,
11
7
  # leading to inconsistent coverage percentages between runs.
12
- #
13
- # need to save coverage/.last_run.json
14
- ret =
15
- system('rm -fr coverage/assets coverage/.*.json.lock coverage/lcov/* coverage/index.html coverage/.resultset.json')
16
- raise unless ret
8
+ raise unless system('rm -fr coverage/')
17
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: checkoff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.2
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vince Broz
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-07-24 00:00:00.000000000 Z
11
+ date: 2022-12-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: fakeweb
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: mdl
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -137,7 +151,7 @@ dependencies:
137
151
  - !ruby/object:Gem::Version
138
152
  version: '0'
139
153
  - !ruby/object:Gem::Dependency
140
- name: mocha
154
+ name: minitest-reporters
141
155
  requirement: !ruby/object:Gem::Requirement
142
156
  requirements:
143
157
  - - ">="
@@ -150,6 +164,20 @@ dependencies:
150
164
  - - ">="
151
165
  - !ruby/object:Gem::Version
152
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: mocha
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 2.0.0.alpha.1
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 2.0.0.alpha.1
153
181
  - !ruby/object:Gem::Dependency
154
182
  name: overcommit
155
183
  requirement: !ruby/object:Gem::Requirement
@@ -193,19 +221,33 @@ dependencies:
193
221
  - !ruby/object:Gem::Version
194
222
  version: '13.0'
195
223
  - !ruby/object:Gem::Dependency
196
- name: rubocop
224
+ name: rspec
197
225
  requirement: !ruby/object:Gem::Requirement
198
226
  requirements:
199
227
  - - ">="
200
228
  - !ruby/object:Gem::Version
201
- version: '0'
229
+ version: '3.4'
202
230
  type: :development
203
231
  prerelease: false
204
232
  version_requirements: !ruby/object:Gem::Requirement
205
233
  requirements:
206
234
  - - ">="
207
235
  - !ruby/object:Gem::Version
208
- version: '0'
236
+ version: '3.4'
237
+ - !ruby/object:Gem::Dependency
238
+ name: rubocop
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - "~>"
242
+ - !ruby/object:Gem::Version
243
+ version: '1.36'
244
+ type: :development
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - "~>"
249
+ - !ruby/object:Gem::Version
250
+ version: '1.36'
209
251
  - !ruby/object:Gem::Dependency
210
252
  name: rubocop-minitest
211
253
  requirement: !ruby/object:Gem::Requirement
@@ -276,6 +318,20 @@ dependencies:
276
318
  - - ">="
277
319
  - !ruby/object:Gem::Version
278
320
  version: '0'
321
+ - !ruby/object:Gem::Dependency
322
+ name: webmock
323
+ requirement: !ruby/object:Gem::Requirement
324
+ requirements:
325
+ - - ">="
326
+ - !ruby/object:Gem::Version
327
+ version: '0'
328
+ type: :development
329
+ prerelease: false
330
+ version_requirements: !ruby/object:Gem::Requirement
331
+ requirements:
332
+ - - ">="
333
+ - !ruby/object:Gem::Version
334
+ version: '0'
279
335
  description:
280
336
  email:
281
337
  - vince@broz.cc
@@ -310,7 +366,6 @@ files:
310
366
  - bin/rake
311
367
  - bin/setup
312
368
  - checkoff.gemspec
313
- - coverage/.last_run.json
314
369
  - docs/cookiecutter_input.json
315
370
  - docs/example_project.png
316
371
  - exe/checkoff
@@ -322,7 +377,11 @@ files:
322
377
  - lib/checkoff/custom_fields.rb
323
378
  - lib/checkoff/internal/config_loader.rb
324
379
  - lib/checkoff/internal/create-class.sh
325
- - lib/checkoff/internal/search_url_parser.rb
380
+ - lib/checkoff/internal/search_url.rb
381
+ - lib/checkoff/internal/search_url/custom_field_param_converter.rb
382
+ - lib/checkoff/internal/search_url/custom_field_variant.rb
383
+ - lib/checkoff/internal/search_url/parser.rb
384
+ - lib/checkoff/internal/search_url/simple_param_converter.rb
326
385
  - lib/checkoff/internal/task_selector_evaluator.rb
327
386
  - lib/checkoff/my_tasks.rb
328
387
  - lib/checkoff/projects.rb
@@ -366,7 +425,8 @@ files:
366
425
  homepage: https://github.com/apiology/checkoff
367
426
  licenses:
368
427
  - MIT license
369
- metadata: {}
428
+ metadata:
429
+ rubygems_mfa_required: 'true'
370
430
  post_install_message:
371
431
  rdoc_options: []
372
432
  require_paths:
@@ -375,14 +435,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
375
435
  requirements:
376
436
  - - ">="
377
437
  - !ruby/object:Gem::Version
378
- version: '2.6'
438
+ version: '2.7'
379
439
  required_rubygems_version: !ruby/object:Gem::Requirement
380
440
  requirements:
381
441
  - - ">="
382
442
  - !ruby/object:Gem::Version
383
443
  version: '0'
384
444
  requirements: []
385
- rubygems_version: 3.0.3.1
445
+ rubygems_version: 3.1.6
386
446
  signing_key:
387
447
  specification_version: 4
388
448
  summary: Command-line and gem client for Asana (unofficial)
@@ -1,6 +0,0 @@
1
- {
2
- "result": {
3
- "line": 99.56,
4
- "branch": 86.11
5
- }
6
- }
@@ -1,228 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- # frozen_string_literal: true
4
-
5
- module Checkoff
6
- module Internal
7
- # base class for handling different custom_field_#{gid}.variant params
8
- class CustomFieldVariant
9
- def initialize(gid, remaining_params)
10
- @gid = gid
11
- @remaining_params = remaining_params
12
- end
13
-
14
- private
15
-
16
- attr_reader :gid, :remaining_params
17
- end
18
-
19
- # custom_field_#{gid}.variant = 'less_than'
20
- class LessThanCustomFieldVariant < CustomFieldVariant
21
- def convert
22
- max_param = "custom_field_#{gid}.max"
23
- case remaining_params.keys
24
- when [max_param]
25
- convert_single_custom_field_less_than_params_max_param(max_param)
26
- else
27
- raise "Teach me how to handle #{remaining_params}"
28
- end
29
- end
30
-
31
- private
32
-
33
- def convert_single_custom_field_less_than_params_max_param(max_param)
34
- max_values = remaining_params.fetch(max_param)
35
- unless max_values.length == 1
36
- raise "Teach me how to handle these remaining keys for #{max_param}: #{remaining_params}"
37
- end
38
-
39
- max_value = max_values[0]
40
- empty_task_selector = []
41
- [{ "custom_fields.#{gid}.less_than" => max_value }, empty_task_selector]
42
- end
43
- end
44
-
45
- # custom_field_#{gid}.variant = 'is_not'
46
- class IsNotCustomFieldVariant < CustomFieldVariant
47
- def convert
48
- case remaining_params.keys
49
- when ["custom_field_#{gid}.selected_options"]
50
- convert_single_custom_field_is_not_params_selected_options
51
- else
52
- raise "Teach me how to handle #{remaining_params}"
53
- end
54
- end
55
-
56
- private
57
-
58
- def convert_single_custom_field_is_not_params_selected_options
59
- selected_options = remaining_params.fetch("custom_field_#{gid}.selected_options")
60
- raise "Teach me how to handle #{remaining_params}" unless selected_options.length == 1
61
-
62
- [{ "custom_fields.#{gid}.is_set" => 'true' },
63
- ['not',
64
- ['custom_field_gid_value_contains_any_gid',
65
- gid,
66
- selected_options.fetch(0).split('~')]]]
67
- end
68
- end
69
-
70
- # custom_field_#{gid}.variant = 'no_value'
71
- class NoValueCustomFieldVariant < CustomFieldVariant
72
- def convert
73
- unless remaining_params.length.zero?
74
- raise "Teach me how to handle these remaining keys for #{variant_key}: #{remaining_params}"
75
- end
76
-
77
- empty_task_selector = []
78
- [{ "custom_fields.#{gid}.is_set" => 'false' }, empty_task_selector]
79
- end
80
- end
81
-
82
- # custom_field_#{gid}.variant = 'is'
83
- class IsCustomFieldVariant < CustomFieldVariant
84
- def convert
85
- unless remaining_params.length == 1
86
- raise "Teach me how to handle these remaining keys for #{variant_key}: #{remaining_params}"
87
- end
88
-
89
- key, values = remaining_params.to_a[0]
90
- convert_custom_field_is_arg(key, values)
91
- end
92
-
93
- private
94
-
95
- def convert_custom_field_is_arg(key, values)
96
- empty_task_selector = []
97
-
98
- if key.end_with? '.selected_options'
99
- raise "Too many values found for #{key}: #{values}" if values.length != 1
100
-
101
- return [{ "custom_fields.#{gid}.value" => values[0] },
102
- empty_task_selector]
103
- end
104
-
105
- raise "Teach me how to handle #{key} = #{values}"
106
- end
107
- end
108
-
109
- # Convert custom field parameters from an Asana search URL into
110
- # API search arguments and Checkoff task selectors
111
- class CustomFieldParamConverter
112
- def initialize(custom_field_params:)
113
- @custom_field_params = custom_field_params
114
- end
115
-
116
- def convert
117
- args = {}
118
- task_selector = []
119
- by_custom_field.each do |gid, single_custom_field_params|
120
- new_args, new_task_selector = convert_single_custom_field_params(gid,
121
- single_custom_field_params)
122
- args, task_selector = merge_args_and_task_selectors(args, new_args,
123
- task_selector, new_task_selector)
124
- end
125
- [args, task_selector]
126
- end
127
-
128
- private
129
-
130
- def by_custom_field
131
- custom_field_params.group_by do |key, _value|
132
- gid_from_custom_field_key(key)
133
- end.transform_values(&:to_h)
134
- end
135
-
136
- def merge_args_and_task_selectors(args, new_args, task_selector, new_task_selector)
137
- args = args.merge(new_args)
138
- return [args, task_selector] if new_task_selector == []
139
-
140
- raise 'Teach me how to merge task selectors' unless task_selector == []
141
-
142
- task_selector = new_task_selector
143
-
144
- [args, task_selector]
145
- end
146
-
147
- VARIANTS = {
148
- 'is' => IsCustomFieldVariant,
149
- 'no_value' => NoValueCustomFieldVariant,
150
- 'is_not' => IsNotCustomFieldVariant,
151
- 'less_than' => LessThanCustomFieldVariant,
152
- }.freeze
153
-
154
- def convert_single_custom_field_params(gid, single_custom_field_params)
155
- variant_key = "custom_field_#{gid}.variant"
156
- variant = single_custom_field_params.fetch(variant_key)
157
- remaining_params = single_custom_field_params.reject { |k, _v| k == variant_key }
158
- raise "Teach me how to handle #{variant_key} = #{variant}" unless variant.length == 1
159
-
160
- variant_class = VARIANTS[variant[0]]
161
- return variant_class.new(gid, remaining_params).convert unless variant_class.nil?
162
-
163
- raise "Teach me how to handle #{variant_key} = #{variant}"
164
- end
165
-
166
- def gid_from_custom_field_key(key)
167
- key.split('_')[2].split('.')[0]
168
- end
169
-
170
- attr_reader :custom_field_params
171
- end
172
-
173
- # Parse Asana search URLs into parameters suitable to pass into
174
- # the /workspaces/{workspace_gid}/tasks/search endpoint
175
- class SearchUrlParser
176
- def initialize(_deps = {}); end
177
-
178
- def convert_params(url)
179
- url_params = CGI.parse(URI.parse(url).query)
180
- custom_field_params, regular_url_params = partition_url_params(url_params)
181
- custom_field_args, task_selector = convert_custom_field_params(custom_field_params)
182
- regular_url_args = convert_regular_params(regular_url_params)
183
- [custom_field_args.merge(regular_url_args), task_selector]
184
- end
185
-
186
- private
187
-
188
- def convert_regular_params(regular_url_params)
189
- regular_url_params.to_a.map do |key, values|
190
- convert_arg(key, values)
191
- end.to_h
192
- end
193
-
194
- def convert_custom_field_params(custom_field_params)
195
- CustomFieldParamConverter.new(custom_field_params: custom_field_params).convert
196
- end
197
-
198
- def partition_url_params(url_params)
199
- url_params.to_a.partition do |key, _values|
200
- key.start_with? 'custom_field_'
201
- end.map(&:to_h)
202
- end
203
-
204
- # https://developers.asana.com/docs/search-tasks-in-a-workspace
205
- def convert_arg(key, values)
206
- case key
207
- when 'any_projects.ids'
208
- ['projects.any', values.join(',')]
209
- when 'completion'
210
- raise "Teach me how to handle #{key} = #{values}" if values.length != 1
211
-
212
- value = values.fetch(0)
213
- raise "Teach me how to handle #{key} = #{values}" if value != 'incomplete'
214
-
215
- ['completed', false]
216
- when 'not_tags.ids'
217
- raise "Teach me how to handle #{key} = #{values}" if values.length != 1
218
-
219
- value = values.fetch(0)
220
- tag_ids = value.split('~')
221
- ['tags.not', tag_ids.join(',')]
222
- else
223
- raise "Teach me how to handle #{key} = #{values}"
224
- end
225
- end
226
- end
227
- end
228
- end