checkoff 0.17.0 → 0.19.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: 934e52a0b06388d6c1dbadb1ff9890cdf74da88f95d9350af04beba49d4021cd
4
- data.tar.gz: 1f8780b895c7be869384afc71e7e1187d7e6594dd9139cc951d5330c46028929
3
+ metadata.gz: 15d4e2598851192dcb2d19e4c2b501d57de3d6deca0f876af8f99402b5d8cb1c
4
+ data.tar.gz: 2a6db316719696b5736cfc3059b947361184ce3218dd835d44eb5db99ccb1df5
5
5
  SHA512:
6
- metadata.gz: ac4dd6171e0867c4209bdd8b3b1e166164ab69b633e283d3985b3054c5a32aa81a076f573034584cfeece30312ea909ce0643850b49e7c5d1436ff75a0bc8cc8
7
- data.tar.gz: 14e79f6ffc230d038cbb514dca84a34465c4f6aab7052b98f6de25dc63b1f3ca1507ab4a1b108da48e918da0f19ac10657d24f3e94d5610881379d0aafca6f4d
6
+ metadata.gz: 9bb9300d617321073e23c103bf87471e1f38562171099c5a1ad010e6d9a06d6d0829a66ced957a15859810bfcffca8b3f4281e580f5bb1b8000593c9a4887331
7
+ data.tar.gz: 91a9629fbd6b42386a4ca8aed172c27b8fe9bf2edd2c7656265051d0dd990ffe12700aaeb1df215c40eb1b64983888adac2252933246f4920a564ba7237c788a
data/.envrc CHANGED
@@ -1,4 +1,4 @@
1
1
  # shellcheck shell=bash
2
2
  PATH_add bin
3
- export ASANA__PERSONAL_ACCESS_TOKEN="op://Private/Asana access token - VLD read-write/token" ASANA__DEFAULT_ASSIGNEE_GID="op://Private/Asana access token - VLD read-write/default_workspace_gid"
3
+ export ASANA__PERSONAL_ACCESS_TOKEN="op://Private/Asana access token - VLD read-write/token" ASANA__DEFAULT_ASSIGNEE_GID="op://Private/Asana access token - VLD read-write/default_workspace_gid" ASANA__DEFAULT_WORKSPACE_GID="op://Private/Asana access token - VLD read-write/default_workspace_gid"
4
4
  direnv_load with-op op run --cache -- direnv dump
data/.gitignore CHANGED
@@ -62,4 +62,3 @@ requirements_dev.txt.installed
62
62
  /coverage/assets/
63
63
  /coverage/index.html
64
64
  /coverage/lcov/
65
- /pkg
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ rule 'MD029', style: 'ordered'
data/.mdlrc ADDED
@@ -0,0 +1 @@
1
+ style ".markdownlint_style.rb"
data/CONTRIBUTING.rst ADDED
@@ -0,0 +1,75 @@
1
+ ============
2
+ Contributing
3
+ ============
4
+
5
+ Contributions are welcome, and they are greatly appreciated! Every little bit
6
+ helps, and credit will always be given.
7
+
8
+ You can contribute in many ways:
9
+
10
+ Types of Contributions
11
+ ----------------------
12
+
13
+ Report Bugs
14
+ ~~~~~~~~~~~
15
+
16
+ Report bugs at https://github.com/apiology/checkoff/issues
17
+
18
+ If you are reporting a bug, please include:
19
+
20
+ * Your operating system name and version.
21
+ * Any details about your local setup that might be helpful in troubleshooting.
22
+ * Detailed steps to reproduce the bug.
23
+
24
+ Fix Bugs
25
+ ~~~~~~~~
26
+
27
+ Look through the GitHub issues for bugs. Anything tagged with "bug"
28
+ and "help wanted" is open to whoever wants to implement a fix for it.
29
+
30
+ Implement Features
31
+ ~~~~~~~~~~~~~~~~~~
32
+
33
+ Look through the GitHub issues for features. Anything tagged with "enhancement"
34
+ and "help wanted" is open to whoever wants to implement it.
35
+
36
+ If you have a new feature in mind, please start it by filing an issue
37
+ so we can discuss the need and approach before you invest time into
38
+ coding it.
39
+
40
+ Write Documentation
41
+ ~~~~~~~~~~~~~~~~~~~
42
+
43
+ Checkoff could always use more documentation, whether as part of
44
+ the official Checkoff docs, in docstrings, or even on the web in blog posts, articles,
45
+ and such.
46
+
47
+ Submit Feedback
48
+ ~~~~~~~~~~~~~~~
49
+
50
+ The best way to send feedback is to file an issue at
51
+ https://github.com/apiology/checkoff/issues.
52
+
53
+ If you are proposing a new feature:
54
+
55
+ * Explain in detail how it would work.
56
+ * Keep the scope as narrow as possible, to make it easier to implement.
57
+ * Remember that this is a volunteer-driven project, and that contributions
58
+ are welcome :)
59
+
60
+ Get Started!
61
+ ------------
62
+
63
+ Ready to contribute? See DEVELOPMENT.md in this directory for details
64
+ on how to set yourself up for local development.
65
+
66
+ Pull Request Guidelines
67
+ -----------------------
68
+
69
+ Before you submit a pull request, check that it meets these guidelines:
70
+
71
+ 1. The pull request should include tests.
72
+
73
+ 2. If the pull request adds functionality, the docs should be updated. Put your
74
+ new functionality into a function with a docstring, and add the feature to
75
+ the list in README.md.
data/DEVELOPMENT.md CHANGED
@@ -57,4 +57,13 @@ git log "v$(bump current)..."
57
57
  bump --tag --tag-prefix=v ${type_of_bump:?}
58
58
  rake release
59
59
  git push
60
+ git push --tags
61
+ ```
62
+
63
+
64
+ ## Developing
65
+
66
+ ```sh
67
+ bundle install
68
+ bundle exec exe/checkoff --help
60
69
  ```
data/Gemfile.lock CHANGED
@@ -12,7 +12,7 @@ GIT
12
12
  PATH
13
13
  remote: .
14
14
  specs:
15
- checkoff (0.17.0)
15
+ checkoff (0.19.0)
16
16
  activesupport
17
17
  asana (> 0.10.0)
18
18
  cache_method
@@ -22,7 +22,7 @@ PATH
22
22
  GEM
23
23
  remote: https://rubygems.org/
24
24
  specs:
25
- activesupport (6.1.5.1)
25
+ activesupport (6.1.6.1)
26
26
  concurrent-ruby (~> 1.0, >= 1.0.2)
27
27
  i18n (>= 1.6, < 2)
28
28
  minitest (>= 5.1)
@@ -33,10 +33,12 @@ GEM
33
33
  cache (0.4.1)
34
34
  cache_method (0.2.7)
35
35
  cache (>= 0.2.1)
36
+ chef-utils (17.10.0)
37
+ concurrent-ruby
36
38
  childprocess (4.0.0)
37
39
  coderay (1.1.3)
38
40
  concurrent-ruby (1.1.10)
39
- dalli (3.2.1)
41
+ dalli (3.2.2)
40
42
  docile (1.4.0)
41
43
  faraday (1.5.1)
42
44
  faraday-em_http (~> 1.0)
@@ -61,15 +63,30 @@ GEM
61
63
  faraday_middleware
62
64
  multi_json
63
65
  gli (2.21.0)
64
- i18n (1.10.0)
66
+ i18n (1.12.0)
65
67
  concurrent-ruby (~> 1.0)
66
68
  imagen (0.1.8)
67
69
  parser (>= 2.5, != 2.5.1.1)
68
70
  iniparse (1.5.0)
69
71
  jwt (2.2.3)
72
+ kramdown (2.4.0)
73
+ rexml
74
+ kramdown-parser-gfm (1.1.0)
75
+ kramdown (~> 2.0)
76
+ mdl (0.11.0)
77
+ kramdown (~> 2.3)
78
+ kramdown-parser-gfm (~> 1.1)
79
+ mixlib-cli (~> 2.1, >= 2.1.1)
80
+ mixlib-config (>= 2.2.1, < 4)
81
+ mixlib-shellout
70
82
  method_source (1.0.0)
71
- minitest (5.15.0)
83
+ minitest (5.16.2)
72
84
  minitest-profile (0.0.2)
85
+ mixlib-cli (2.1.8)
86
+ mixlib-config (3.0.27)
87
+ tomlrb
88
+ mixlib-shellout (3.2.7)
89
+ chef-utils
73
90
  mocha (1.12.0)
74
91
  multi_json (1.15.0)
75
92
  multi_xml (0.6.0)
@@ -90,7 +107,7 @@ GEM
90
107
  pry (0.14.1)
91
108
  coderay (~> 1.1)
92
109
  method_source (~> 1.0)
93
- rack (2.2.3)
110
+ rack (2.2.4)
94
111
  rainbow (3.0.0)
95
112
  rake (13.0.3)
96
113
  regexp_parser (2.1.1)
@@ -120,14 +137,15 @@ GEM
120
137
  simplecov-html (0.12.3)
121
138
  simplecov-lcov (0.8.0)
122
139
  simplecov_json_formatter (0.1.3)
123
- tzinfo (2.0.4)
140
+ tomlrb (2.0.3)
141
+ tzinfo (2.0.5)
124
142
  concurrent-ruby (~> 1.0)
125
143
  undercover (0.4.3)
126
144
  imagen (>= 0.1.8)
127
145
  rainbow (>= 2.1, < 4.0)
128
146
  rugged (>= 0.27, < 1.2)
129
147
  unicode-display_width (2.0.0)
130
- zeitwerk (2.5.4)
148
+ zeitwerk (2.6.0)
131
149
 
132
150
  PLATFORMS
133
151
  ruby
@@ -140,6 +158,7 @@ DEPENDENCIES
140
158
  bump
141
159
  bundler
142
160
  checkoff!
161
+ mdl
143
162
  minitest-profile
144
163
  mocha
145
164
  overcommit (>= 0.58.0)
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021, Vince Broz
3
+ Copyright (c) 2016-2022, Vince Broz
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
-
data/Makefile CHANGED
@@ -39,6 +39,9 @@ clear_metrics: ## remove or reset result artifacts created by tests and quality
39
39
 
40
40
  clean: clear_metrics ## remove all built artifacts
41
41
 
42
+
43
+ typecheck: ## validate types in code and configuration
44
+
42
45
  overcommit: ## run precommit quality checks
43
46
  bundle exec overcommit --run
44
47
 
data/README.md CHANGED
@@ -149,18 +149,14 @@ personal_access_token: 'some_big_long_string_from_asana.com_here'
149
149
 
150
150
  Alternately you can set environment variables to match - e.g., `ASANA__PERSONAL_ACCESS_TOKEN`
151
151
 
152
- ## Developing
152
+ ## Contributions
153
153
 
154
- ```sh
155
- bundle install
156
- bundle exec exe/checkoff --help
157
- ```
154
+ This project, as with all others, rests on the shoulders of a broad
155
+ ecosystem supported by many volunteers doing thankless work, along
156
+ with specific contributors.
158
157
 
159
- To publish new version as a maintainer:
158
+ In particular I'd like to call out:
160
159
 
161
- ```sh
162
- git log "v$(bump current)..."
163
- # Set type_of_bump to patch, minor, or major
164
- bump --tag --tag-prefix=v ${type_of_bump:?}
165
- rake release
166
- ```
160
+ * [Audrey Roy Greenfeld](https://github.com/audreyfeldroy) for the
161
+ cookiecutter tool and associated examples, which keep my many
162
+ projects building with shared boilerplate with a minimum of fuss.
data/checkoff.gemspec CHANGED
@@ -32,6 +32,7 @@ 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 'mdl'
35
36
  spec.add_development_dependency 'minitest-profile'
36
37
  spec.add_development_dependency 'mocha'
37
38
  # 0.58.0 and 0.57.0 don't seem super compatible with signatures, and
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "result": {
3
- "line": 99.78,
4
- "branch": 93.33
3
+ "line": 99.61,
4
+ "branch": 88.05
5
5
  }
6
6
  }
data/fix.sh CHANGED
@@ -284,12 +284,7 @@ ensure_python_versions() {
284
284
  }
285
285
 
286
286
  major_minor="$(cut -d. -f1-2 <<<"${ver}")"
287
- if [ "${major_minor}" == 3.6 ]
288
- then
289
- pyenv_install --patch "${ver}" < <(curl -sSL https://github.com/python/cpython/commit/8ea6353.patch\?full_index=1)
290
- else
291
- pyenv_install "${ver}"
292
- fi
287
+ pyenv_install "${ver}"
293
288
  else
294
289
  pyenv install -s "${ver}"
295
290
  fi
@@ -307,12 +302,12 @@ ensure_pyenv_virtualenvs() {
307
302
  }
308
303
 
309
304
  ensure_pip_and_wheel() {
310
- # Make sure we have a pip with the 20.3 resolver, and after the
311
- # initial bugfix release
305
+ # pip 22 seems to be better at finding pandas pre-compiled wheels
306
+ # for macOS, so let's make sure we're using at least that version
312
307
  major_pip_version=$(pip --version | cut -d' ' -f2 | cut -d '.' -f 1)
313
308
  if [[ major_pip_version -lt 21 ]]
314
309
  then
315
- pip install 'pip>=20.3.1'
310
+ pip install 'pip>=22'
316
311
  fi
317
312
  # wheel is helpful for being able to cache long package builds
318
313
  pip show wheel >/dev/null 2>&1 || pip install wheel
data/lib/checkoff/cli.rb CHANGED
@@ -61,7 +61,7 @@ module Checkoff
61
61
  to_workspace_arg:,
62
62
  to_project_arg:,
63
63
  to_section_arg:,
64
- config: Checkoff::ConfigLoader.load(:asana),
64
+ config: Checkoff::Internal::ConfigLoader.load(:asana),
65
65
  projects: Checkoff::Projects.new(config: config),
66
66
  sections: Checkoff::Sections.new(config: config),
67
67
  logger: $stderr)
@@ -122,7 +122,7 @@ module Checkoff
122
122
  class ViewSubcommand
123
123
  def initialize(workspace_name, project_name, section_name,
124
124
  task_name,
125
- config: Checkoff::ConfigLoader.load(:asana),
125
+ config: Checkoff::Internal::ConfigLoader.load(:asana),
126
126
  projects: Checkoff::Projects.new(config: config),
127
127
  sections: Checkoff::Sections.new(config: config,
128
128
  projects: projects),
@@ -201,7 +201,7 @@ module Checkoff
201
201
  # CLI subcommand that creates a task
202
202
  class QuickaddSubcommand
203
203
  def initialize(workspace_name, task_name,
204
- config: Checkoff::ConfigLoader.load(:asana),
204
+ config: Checkoff::Internal::ConfigLoader.load(:asana),
205
205
  workspaces: Checkoff::Workspaces.new(config: config),
206
206
  tasks: Checkoff::Tasks.new(config: config))
207
207
  @workspace_name = workspace_name
@@ -3,7 +3,7 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  require 'forwardable'
6
- require_relative 'config_loader'
6
+ require_relative 'internal/config_loader'
7
7
  require 'asana'
8
8
 
9
9
  # https://developers.asana.com/docs/clients
@@ -18,7 +18,7 @@ module Checkoff
18
18
  LONG_CACHE_TIME = MINUTE * 15
19
19
  SHORT_CACHE_TIME = MINUTE
20
20
 
21
- def initialize(config: Checkoff::ConfigLoader.load(:asana),
21
+ def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
22
22
  asana_client_class: Asana::Client)
23
23
  @config = config
24
24
  @asana_client_class = asana_client_class
@@ -16,7 +16,7 @@ cat > "${underscored_plural_name}.rb" << EOF
16
16
 
17
17
  require 'forwardable'
18
18
  require 'cache_method'
19
- require_relative 'config_loader'
19
+ require_relative 'internal/config_loader'
20
20
  require_relative 'workspaces'
21
21
  require_relative 'clients'
22
22
 
@@ -31,7 +31,7 @@ module Checkoff
31
31
  LONG_CACHE_TIME = MINUTE * 15
32
32
  SHORT_CACHE_TIME = MINUTE
33
33
 
34
- def initialize(config: Checkoff::ConfigLoader.load(:asana),
34
+ def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
35
35
  workspaces: Checkoff::Workspaces.new(config: config),
36
36
  clients: Checkoff::Clients.new(config: config),
37
37
  client: clients.client)
@@ -4,7 +4,7 @@
4
4
 
5
5
  require 'forwardable'
6
6
  require 'cache_method'
7
- require_relative 'config_loader'
7
+ require_relative 'internal/config_loader'
8
8
  require_relative 'workspaces'
9
9
  require_relative 'clients'
10
10
 
@@ -20,7 +20,7 @@ module Checkoff
20
20
  LONG_CACHE_TIME = MINUTE * 15
21
21
  SHORT_CACHE_TIME = MINUTE
22
22
 
23
- def initialize(config: Checkoff::ConfigLoader.load(:asana),
23
+ def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
24
24
  workspaces: Checkoff::Workspaces.new(config: config),
25
25
  clients: Checkoff::Clients.new(config: config),
26
26
  client: clients.client)
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'active_support/core_ext/hash'
5
+
6
+ module Checkoff
7
+ module Internal
8
+ # Use the provided config from a YAML file, and fall back to env
9
+ # variable if it's not populated for a key'
10
+ class EnvFallbackConfigLoader
11
+ def initialize(config, sym, yaml_filename)
12
+ @config = config
13
+ @envvar_prefix = sym.upcase
14
+ @yaml_filename = yaml_filename
15
+ end
16
+
17
+ def [](key)
18
+ config_value = @config[key]
19
+ return config_value unless config_value.nil?
20
+
21
+ ENV[envvar_name(key)]
22
+ end
23
+
24
+ def fetch(key)
25
+ out = self[key]
26
+ return out unless out.nil?
27
+
28
+ raise KeyError,
29
+ "Please configure either the #{key} key in #{@yaml_filename} or set #{envvar_name(key)}"
30
+ end
31
+
32
+ private
33
+
34
+ def envvar_name(key)
35
+ "#{@envvar_prefix}__#{key.upcase}"
36
+ end
37
+ end
38
+
39
+ # Load configuration file
40
+ class ConfigLoader
41
+ class << self
42
+ def load(sym)
43
+ yaml_result = load_yaml_file(sym)
44
+ EnvFallbackConfigLoader.new(yaml_result, sym, yaml_filename(sym))
45
+ end
46
+
47
+ private
48
+
49
+ def load_yaml_file(sym)
50
+ filename = yaml_filename(sym)
51
+ return {} unless File.exist?(filename)
52
+
53
+ YAML.load_file(filename).with_indifferent_access
54
+ end
55
+
56
+ def yaml_filename(sym)
57
+ file = "#{sym}.yml"
58
+ File.expand_path("~/.#{file}")
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,22 @@
1
+ #!/bin/bash -eu
2
+
3
+ set -o pipefail
4
+
5
+ underscored_plural_name="${1:?underscored plural name of entities minus .rb}"
6
+ class_name="${2:?class name without Checkoff:: prefix}"
7
+
8
+ cat > "${underscored_plural_name}.rb" << EOF
9
+ #!/usr/bin/env ruby
10
+
11
+ # frozen_string_literal: true
12
+
13
+ module Checkoff
14
+ module Internal
15
+ class ${class_name}
16
+ def initialize(_deps = {}); end
17
+ end
18
+ end
19
+ end
20
+ EOF
21
+
22
+ git add "${underscored_plural_name}.rb"
@@ -0,0 +1,213 @@
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
+ PARAM_MAP = {
205
+ 'any_projects.ids' => 'projects.any',
206
+ }.freeze
207
+
208
+ def convert_arg(key, values)
209
+ [PARAM_MAP.fetch(key), values.join(',')]
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkoff
4
+ # Base class to evaluate a task selector function given fully evaluated arguments
5
+ class FunctionEvaluator
6
+ def initialize(task_selector:)
7
+ @task_selector = task_selector
8
+ end
9
+
10
+ def evaluate_arg?(_index)
11
+ true
12
+ end
13
+
14
+ private
15
+
16
+ def fn?(object, fn_name)
17
+ object.is_a?(Array) && !object.empty? && [fn_name, fn_name.to_s].include?(object[0])
18
+ end
19
+
20
+ attr_reader :task_selector
21
+ end
22
+
23
+ # :not function
24
+ class NotFunctionEvaluator < FunctionEvaluator
25
+ def matches?
26
+ fn?(task_selector, :not)
27
+ end
28
+
29
+ def evaluate(_task, subvalue)
30
+ !subvalue
31
+ end
32
+ end
33
+
34
+ # :nil? function
35
+ class NilPFunctionEvaluator < FunctionEvaluator
36
+ def matches?
37
+ fn?(task_selector, :nil?)
38
+ end
39
+
40
+ def evaluate(_task, subvalue)
41
+ subvalue.nil?
42
+ end
43
+ end
44
+
45
+ # :tag function
46
+ class TagPFunctionEvaluator < FunctionEvaluator
47
+ def matches?
48
+ fn?(task_selector, :tag)
49
+ end
50
+
51
+ def evaluate_arg?(_index)
52
+ false
53
+ end
54
+
55
+ def evaluate(task, tag_name)
56
+ task.tags.map(&:name).include? tag_name
57
+ end
58
+ end
59
+
60
+ # :custom_field_value function
61
+ class CustomFieldValueFunctionEvaluator < FunctionEvaluator
62
+ def matches?
63
+ fn?(task_selector, :custom_field_value)
64
+ end
65
+
66
+ def evaluate_arg?(_index)
67
+ false
68
+ end
69
+
70
+ def evaluate(task, custom_field_name)
71
+ custom_fields = task.custom_fields
72
+ if custom_fields.nil?
73
+ raise "custom fields not found on task - did you add 'custom_field' in your extra_fields argument?"
74
+ end
75
+
76
+ custom_field = custom_fields.find { |field| field.fetch('name') == custom_field_name }
77
+ return nil if custom_field.nil?
78
+
79
+ custom_field['display_value']
80
+ end
81
+ end
82
+
83
+ # :custom_field_gid_value_contains_any_gid function
84
+ class CustomFieldGidValueContainsAnyGidFunctionEvaluator < FunctionEvaluator
85
+ def matches?
86
+ fn?(task_selector, :custom_field_gid_value_contains_any_gid)
87
+ end
88
+
89
+ def evaluate_arg?(_index)
90
+ false
91
+ end
92
+
93
+ def evaluate(task, custom_field_gid, custom_field_values_gids)
94
+ actual_custom_field_values_gids = pull_custom_field_values_gids(task, custom_field_gid)
95
+
96
+ (custom_field_values_gids - actual_custom_field_values_gids).empty?
97
+ end
98
+
99
+ private
100
+
101
+ def pull_custom_field_or_raise(task, custom_field_gid)
102
+ custom_fields = task.custom_fields
103
+ if custom_fields.nil?
104
+ raise "Could not find custom_fields under task (was 'custom_fields' included in 'extra_fields'?)"
105
+ end
106
+
107
+ matched_custom_field = custom_fields.find { |data| data.fetch('gid') == custom_field_gid }
108
+ raise "Could not find custom field with gid #{custom_field_gid}" if matched_custom_field.nil?
109
+
110
+ matched_custom_field
111
+ end
112
+
113
+ def pull_custom_field_values_gids(task, custom_field_gid)
114
+ matched_custom_field = pull_custom_field_or_raise(task, custom_field_gid)
115
+
116
+ enum_value = matched_custom_field.fetch('enum_value')
117
+ actual_custom_field_values_gids = []
118
+ unless enum_value.nil?
119
+ if enum_value.fetch('enabled') == false
120
+ raise "Unexpected enabled value on custom field: #{matched_custom_field}"
121
+ end
122
+
123
+ actual_custom_field_values_gids = [enum_value.fetch('gid')]
124
+ end
125
+ actual_custom_field_values_gids
126
+ end
127
+ end
128
+
129
+ # Evaluator task selectors against a task
130
+ class TaskSelectorEvaluator
131
+ def initialize(task:)
132
+ @task = task
133
+ end
134
+
135
+ FUNCTION_EVALUTORS = [
136
+ NotFunctionEvaluator,
137
+ NilPFunctionEvaluator,
138
+ TagPFunctionEvaluator,
139
+ CustomFieldValueFunctionEvaluator,
140
+ CustomFieldGidValueContainsAnyGidFunctionEvaluator,
141
+ ].freeze
142
+
143
+ def evaluate(task_selector)
144
+ return true if task_selector == []
145
+
146
+ FUNCTION_EVALUTORS.each do |evaluator_class|
147
+ evaluator = evaluator_class.new(task_selector: task_selector)
148
+
149
+ next unless evaluator.matches?
150
+
151
+ return try_this_evaluator(task_selector, evaluator)
152
+ end
153
+
154
+ raise "Syntax issue trying to handle #{task_selector}"
155
+ end
156
+
157
+ private
158
+
159
+ def try_this_evaluator(task_selector, evaluator)
160
+ evaluated_args = task_selector[1..].map.with_index do |item, index|
161
+ if evaluator.evaluate_arg?(index)
162
+ evaluate(item)
163
+ else
164
+ item
165
+ end
166
+ end
167
+
168
+ evaluator.evaluate(task, *evaluated_args)
169
+ end
170
+
171
+ attr_reader :task, :task_selector
172
+ end
173
+ end
@@ -14,7 +14,7 @@ module Checkoff
14
14
 
15
15
  attr_reader :projects
16
16
 
17
- def initialize(config: Checkoff::ConfigLoader.load(:asana),
17
+ def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
18
18
  projects: Checkoff::Projects.new(config: config))
19
19
  @config = config
20
20
  @projects = projects
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'config_loader'
3
+ require_relative 'internal/config_loader'
4
4
  require_relative 'workspaces'
5
5
  require_relative 'clients'
6
6
  require 'cache_method'
@@ -23,7 +23,7 @@ module Checkoff
23
23
  LONG_CACHE_TIME = MINUTE * 15
24
24
  SHORT_CACHE_TIME = MINUTE * 5
25
25
 
26
- def initialize(config: Checkoff::ConfigLoader.load(:asana),
26
+ def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
27
27
  workspaces: Checkoff::Workspaces.new(config: config),
28
28
  clients: Checkoff::Clients.new(config: config),
29
29
  client: clients.client)
@@ -17,7 +17,7 @@ module Checkoff
17
17
 
18
18
  attr_reader :projects, :workspaces, :time, :my_tasks
19
19
 
20
- def initialize(config: Checkoff::ConfigLoader.load(:asana),
20
+ def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
21
21
  projects: Checkoff::Projects.new(config: config),
22
22
  workspaces: Checkoff::Workspaces.new(config: config),
23
23
  clients: Checkoff::Clients.new(config: config),
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'forwardable'
4
- require_relative 'config_loader'
4
+ require_relative 'internal/config_loader'
5
5
  require_relative 'projects'
6
6
 
7
7
  module Checkoff
@@ -13,7 +13,7 @@ module Checkoff
13
13
 
14
14
  extend Forwardable
15
15
 
16
- def initialize(config: Checkoff::ConfigLoader.load(:asana),
16
+ def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
17
17
  projects: Checkoff::Projects.new(config: config))
18
18
  @projects = projects
19
19
  end
data/lib/checkoff/tags.rb CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  require 'forwardable'
6
6
  require 'cache_method'
7
- require_relative 'config_loader'
7
+ require_relative 'internal/config_loader'
8
8
  require_relative 'workspaces'
9
9
  require_relative 'clients'
10
10
 
@@ -20,7 +20,7 @@ module Checkoff
20
20
  LONG_CACHE_TIME = MINUTE * 15
21
21
  SHORT_CACHE_TIME = MINUTE
22
22
 
23
- def initialize(config: Checkoff::ConfigLoader.load(:asana),
23
+ def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
24
24
  workspaces: Checkoff::Workspaces.new(config: config),
25
25
  clients: Checkoff::Clients.new(config: config),
26
26
  client: clients.client)
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require 'forwardable'
6
+ require 'cache_method'
7
+ require_relative 'internal/config_loader'
8
+ require_relative 'workspaces'
9
+ require_relative 'projects'
10
+ require_relative 'clients'
11
+ require_relative 'task_selectors'
12
+ require 'asana/resource_includes/collection'
13
+ require 'asana/resource_includes/response_helper'
14
+
15
+ require 'checkoff/internal/search_url_parser'
16
+
17
+ # https://developers.asana.com/docs/task-searches
18
+ module Checkoff
19
+ # Run task searches against the Asana API
20
+ class TaskSearches
21
+ MINUTE = 60
22
+ HOUR = MINUTE * 60
23
+ DAY = 24 * HOUR
24
+ REALLY_LONG_CACHE_TIME = HOUR * 1
25
+ LONG_CACHE_TIME = MINUTE * 15
26
+ SHORT_CACHE_TIME = MINUTE
27
+
28
+ include Asana::Resources::ResponseHelper
29
+
30
+ def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
31
+ workspaces: Checkoff::Workspaces.new(config: config),
32
+ task_selectors: Checkoff::TaskSelectors.new(config: config),
33
+ projects: Checkoff::Projects.new(config: config),
34
+ clients: Checkoff::Clients.new(config: config),
35
+ client: clients.client,
36
+ search_url_parser: Checkoff::Internal::SearchUrlParser.new,
37
+ asana_resources_collection_class: Asana::Resources::Collection)
38
+ @workspaces = workspaces
39
+ @task_selectors = task_selectors
40
+ @projects = projects
41
+ @client = client
42
+ @search_url_parser = search_url_parser
43
+ @asana_resources_collection_class = asana_resources_collection_class
44
+ end
45
+
46
+ def task_search(workspace_name, url, extra_fields: [])
47
+ workspace = workspaces.workspace_or_raise(workspace_name)
48
+ api_params, task_selector = @search_url_parser.convert_params(url)
49
+ path = "/workspaces/#{workspace.gid}/tasks/search"
50
+ options = calculate_api_options(extra_fields)
51
+ tasks = @asana_resources_collection_class.new(parse(client.get(path,
52
+ params: api_params,
53
+ options: options)),
54
+ type: Asana::Resources::Task,
55
+ client: client)
56
+ tasks.select { |task| task_selectors.filter_via_task_selector(task, task_selector) }
57
+ end
58
+ cache_method :task_search, LONG_CACHE_TIME
59
+
60
+ private
61
+
62
+ def calculate_api_options(extra_fields)
63
+ options = projects.task_options[:options]
64
+ options[:fields] += ['custom_fields']
65
+ options[:fields] += extra_fields
66
+ options
67
+ end
68
+
69
+ # bundle exec ./task_searches.rb
70
+ # :nocov:
71
+ class << self
72
+ def run
73
+ workspace_name = ARGV[0] || raise('Please pass workspace name as first argument')
74
+ url = ARGV[1] || raise('Please pass task search URL as second argument')
75
+ task_searches = Checkoff::TaskSearches.new
76
+ task_search = task_searches.task_search(workspace_name, url)
77
+ puts "Results: #{task_search}"
78
+ end
79
+ end
80
+ # :nocov:
81
+
82
+ attr_reader :task_selectors, :projects, :workspaces, :client
83
+ end
84
+ end
85
+
86
+ # :nocov:
87
+ abs_program_name = File.expand_path($PROGRAM_NAME)
88
+ Checkoff::TaskSearches.run if abs_program_name == __FILE__
89
+ # :nocov:
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require 'forwardable'
6
+ require 'cache_method'
7
+ require 'json'
8
+ require_relative 'internal/config_loader'
9
+ require_relative 'internal/task_selector_evaluator'
10
+
11
+ # https://developers.asana.com/docs/task-selectors
12
+
13
+ module Checkoff
14
+ # Filter lists of tasks using declarative selectors.
15
+ class TaskSelectors
16
+ MINUTE = 60
17
+ HOUR = MINUTE * 60
18
+ DAY = 24 * HOUR
19
+ REALLY_LONG_CACHE_TIME = HOUR * 1
20
+ LONG_CACHE_TIME = MINUTE * 15
21
+ SHORT_CACHE_TIME = MINUTE
22
+
23
+ def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana))
24
+ @config = config
25
+ end
26
+
27
+ # @param [Hash<Symbol, Object>] task_selector Filter based on
28
+ # description. Examples: {tag: 'foo'} {:not {tag: 'foo'} (:tag 'foo')
29
+ def filter_via_task_selector(task, task_selector)
30
+ evaluator = TaskSelectorEvaluator.new(task: task)
31
+ evaluator.evaluate(task_selector)
32
+ end
33
+
34
+ # bundle exec ./task_selectors.rb
35
+ # :nocov:
36
+ class << self
37
+ def project_name
38
+ ARGV[1] || raise('Please pass project name to pull tasks from as first argument')
39
+ end
40
+
41
+ def workspace_name
42
+ ARGV[0] || raise('Please pass workspace name as first argument')
43
+ end
44
+
45
+ def task_selector
46
+ task_selector_json = ARGV[2] || raise('Please pass task_selector in JSON form as third argument')
47
+ JSON.parse(task_selector_json)
48
+ end
49
+
50
+ def run
51
+ require 'checkoff/projects'
52
+
53
+ task_selectors = Checkoff::TaskSelectors.new
54
+ extra_fields = ['custom_fields']
55
+ projects = Checkoff::Projects.new
56
+ project = projects.project_or_raise(workspace_name, project_name)
57
+ raw_tasks = projects.tasks_from_project(project, extra_fields: extra_fields)
58
+ tasks = raw_tasks.filter { |task| task_selectors.filter_via_task_selector(task, task_selector) }
59
+ # avoid n+1 queries generating the full task formatting
60
+ puts JSON.pretty_generate(tasks.map(&:to_h))
61
+ end
62
+ end
63
+ # :nocov:
64
+ end
65
+ end
66
+
67
+ # :nocov:
68
+ abs_program_name = File.expand_path($PROGRAM_NAME)
69
+ Checkoff::TaskSelectors.run if abs_program_name == __FILE__
70
+ # :nocov:
@@ -14,7 +14,7 @@ module Checkoff
14
14
  LONG_CACHE_TIME = MINUTE * 15
15
15
  SHORT_CACHE_TIME = MINUTE * 5
16
16
 
17
- def initialize(config: Checkoff::ConfigLoader.load(:asana),
17
+ def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
18
18
  sections: Checkoff::Sections.new(config: config),
19
19
  clients: Checkoff::Clients.new(config: config),
20
20
  client: clients.client,
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Command-line and gem client for Asana (unofficial)
3
4
  module Checkoff
4
5
  # Version of library
5
- VERSION = '0.17.0'
6
+ VERSION = '0.19.0'
6
7
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'forwardable'
4
4
  require 'cache_method'
5
- require_relative 'config_loader'
5
+ require_relative 'internal/config_loader'
6
6
  require_relative 'clients'
7
7
 
8
8
  # https://developers.asana.com/docs/workspaces
@@ -17,7 +17,7 @@ module Checkoff
17
17
  LONG_CACHE_TIME = MINUTE * 15
18
18
  SHORT_CACHE_TIME = MINUTE
19
19
 
20
- def initialize(config: Checkoff::ConfigLoader.load(:asana),
20
+ def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
21
21
  clients: Checkoff::Clients.new(config: config),
22
22
  client: clients.client)
23
23
  @config = config
data/lib/checkoff.rb CHANGED
@@ -8,3 +8,4 @@ require 'checkoff/subtasks'
8
8
  require 'checkoff/tasks'
9
9
  require 'checkoff/custom_fields'
10
10
  require 'checkoff/tags'
11
+ require 'checkoff/task_selectors'
data/rakelib/console.rake CHANGED
@@ -2,5 +2,14 @@
2
2
 
3
3
  desc 'Load up checkoff in pry'
4
4
  task :console do |_t|
5
+ puts 'Example:'
6
+ puts
7
+ puts '# https://www.rubydoc.info/github/Asana/ruby-asana/master'
8
+ puts '> client = Checkoff::Clients.new.client'
9
+ puts '# https://developers.asana.com/docs/input-output-options'
10
+ puts '> workspace_gid = ENV.fetch("ASANA__DEFAULT_WORKSPACE_GID")'
11
+ puts "> task = client.tasks.find_by_id('1199961990964812', options: { fields: ['dependencies'] })"
12
+ puts '# Asana.md#ruby-asana / API shows how to manually call endpoints with a client'
13
+ puts
5
14
  exec 'pry -I lib -r checkoff'
6
15
  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.17.0
4
+ version: 0.19.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-05-07 00:00:00.000000000 Z
11
+ date: 2022-07-23 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: mdl
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: minitest-profile
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -275,10 +289,13 @@ files:
275
289
  - ".git-hooks/pre_commit/circle_ci.rb"
276
290
  - ".gitattributes"
277
291
  - ".gitignore"
292
+ - ".markdownlint_style.rb"
293
+ - ".mdlrc"
278
294
  - ".overcommit.yml"
279
295
  - ".rubocop.yml"
280
296
  - ".yamllint.yml"
281
297
  - CODE_OF_CONDUCT.md
298
+ - CONTRIBUTING.rst
282
299
  - DEVELOPMENT.md
283
300
  - Gemfile
284
301
  - Gemfile.lock
@@ -301,14 +318,19 @@ files:
301
318
  - lib/checkoff.rb
302
319
  - lib/checkoff/cli.rb
303
320
  - lib/checkoff/clients.rb
304
- - lib/checkoff/config_loader.rb
305
321
  - lib/checkoff/create-entity.sh
306
322
  - lib/checkoff/custom_fields.rb
323
+ - lib/checkoff/internal/config_loader.rb
324
+ - lib/checkoff/internal/create-class.sh
325
+ - lib/checkoff/internal/search_url_parser.rb
326
+ - lib/checkoff/internal/task_selector_evaluator.rb
307
327
  - lib/checkoff/my_tasks.rb
308
328
  - lib/checkoff/projects.rb
309
329
  - lib/checkoff/sections.rb
310
330
  - lib/checkoff/subtasks.rb
311
331
  - lib/checkoff/tags.rb
332
+ - lib/checkoff/task_searches.rb
333
+ - lib/checkoff/task_selectors.rb
312
334
  - lib/checkoff/tasks.rb
313
335
  - lib/checkoff/version.rb
314
336
  - lib/checkoff/workspaces.rb
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'yaml'
4
- require 'active_support/core_ext/hash'
5
-
6
- module Checkoff
7
- # Use the provided config from a YAML file, and fall back to env
8
- # variable if it's not populated for a key'
9
- class EnvFallbackConfigLoader
10
- def initialize(config, sym, yaml_filename)
11
- @config = config
12
- @envvar_prefix = sym.upcase
13
- @yaml_filename = yaml_filename
14
- end
15
-
16
- def [](key)
17
- config_value = @config[key]
18
- return config_value unless config_value.nil?
19
-
20
- ENV[envvar_name(key)]
21
- end
22
-
23
- def fetch(key)
24
- out = self[key]
25
- return out unless out.nil?
26
-
27
- raise KeyError,
28
- "Please configure either the #{key} key in #{@yaml_filename} or set #{envvar_name(key)}"
29
- end
30
-
31
- private
32
-
33
- def envvar_name(key)
34
- "#{@envvar_prefix}__#{key.upcase}"
35
- end
36
- end
37
-
38
- # Load configuration file
39
- class ConfigLoader
40
- class << self
41
- def load(sym)
42
- yaml_result = load_yaml_file(sym)
43
- EnvFallbackConfigLoader.new(yaml_result, sym, yaml_filename(sym))
44
- end
45
-
46
- private
47
-
48
- def load_yaml_file(sym)
49
- filename = yaml_filename(sym)
50
- return {} unless File.exist?(filename)
51
-
52
- YAML.load_file(filename).with_indifferent_access
53
- end
54
-
55
- def yaml_filename(sym)
56
- file = "#{sym}.yml"
57
- File.expand_path("~/.#{file}")
58
- end
59
- end
60
- end
61
- end