rtasklib 0.1.4 → 0.1.5

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
  SHA1:
3
- metadata.gz: 6594a20f1c965ccf953918bf9fb7eba89832dc70
4
- data.tar.gz: 6c889440f54c76d7eac9f46e62ad1d7300e44399
3
+ metadata.gz: 6dd45dd292c87269556e732b902f18cf38a098eb
4
+ data.tar.gz: a8adeee47b89eb6cadc2a5cf79e4825711f4e181
5
5
  SHA512:
6
- metadata.gz: 23c9ecccf416dcf6e94835258096ec96dc5257d9ee1109a31014c11100bfd329b11b02ca42a59d2c72c095b6f360b21ddbdc5db1726cdf0c21dc61a6fb127b5b
7
- data.tar.gz: b9962b934cca761f7a8e3c7ad623d76238a1b10722e0e1eb876c75d06640cf5b326e9339f31da34cfe73c12669f7b322d7e1f2652eef35afaf0c1782380aa670
6
+ metadata.gz: f2ad056508be0fabd7365c5431abe144220283ea6ba7beb77718d807f1549307876f1cde5708342339a50b3c4e6fa9b0ce4d9781b52c670020d55397850d5678
7
+ data.tar.gz: 8afd466375b305d8ed56fe5e98b66328391063c864db40637a02ecb9f65b58bfc4b6f49542977a739a7f44ccd394744deb9e0d5a7fe7bd2e3ff7b028eb2459c1
data/.gitignore CHANGED
@@ -6,7 +6,6 @@
6
6
  /pkg/
7
7
  /doc/
8
8
  /spec/reports/
9
+ /spec/data/
9
10
  /tmp/
10
11
  *.py
11
- lib/rtasklib/marshallable.rb
12
- spec/marshallable_spec.rb
data/.travis.yml CHANGED
@@ -25,6 +25,8 @@ before_install:
25
25
  - export TASKRC=/home/travis/build/dropofwill/rtasklib/spec/data/.taskrc
26
26
  - export TASKDATA=/home/travis/build/dropofwill/rtasklib/spec/data/.task
27
27
  - task _version
28
+ - task export
29
+ - task add "Test task persistence"
28
30
 
29
31
  script: 'bundle exec rake'
30
32
 
data/README.md CHANGED
@@ -36,7 +36,18 @@ Or install it yourself as:
36
36
 
37
37
  ## Usage
38
38
 
39
- TODO: Write usage instructions here
39
+ ```
40
+ require 'rtasklib'
41
+
42
+ tw = Rtasklib::TW.new('../path/to/.task')
43
+
44
+ # do some stuff with the task database
45
+ # available commands are documented in the Controller class
46
+
47
+ tw.all
48
+ ```
49
+
50
+ [Controller docs](http://will-paul.com/rtasklib/Rtasklib/Controller.html)
40
51
 
41
52
 
42
53
  ## Development
data/Rakefile CHANGED
@@ -1,5 +1,7 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
+ require "open3"
4
+ require "pp"
3
5
 
4
6
  # run tests with `rake spec`
5
7
  RSpec::Core::RakeTask.new :spec do |task|
@@ -10,9 +12,12 @@ task default: :spec
10
12
 
11
13
  desc "Update and publish docs to gh-pages"
12
14
  task :docs do |task|
13
- `yard docs`
14
- `ghp-import -p doc`
15
- end
15
+ o, s = Open3.capture2("yard doc")
16
+ o.split("\n").each { |line| p line }
17
+ p ""
16
18
 
17
- task :build_rpm do
19
+ Open3.capture2("ghp-import -p doc")
18
20
  end
21
+
22
+ # task :build_rpm do
23
+ # end
data/lib/rtasklib.rb CHANGED
@@ -2,15 +2,15 @@ require_relative "rtasklib/version"
2
2
  require_relative "rtasklib/models"
3
3
  require_relative "rtasklib/execute"
4
4
  require_relative "rtasklib/controller"
5
+ require_relative "rtasklib/helpers"
5
6
  require_relative "rtasklib/taskrc"
6
7
 
7
- require "open3"
8
8
  require "pathname"
9
9
 
10
10
  module Rtasklib
11
11
 
12
12
  class TaskWarrior
13
- attr_reader :version, :data_location, :taskrc,
13
+ attr_reader :version, :data_location, :taskrc, :udas,
14
14
  :override, :override_a, :override_str
15
15
 
16
16
  include Controller
@@ -25,22 +25,24 @@ module Rtasklib
25
25
  LOWEST_VERSION = Gem::Version.new('2.4.0')
26
26
 
27
27
  def initialize data="#{Dir.home}/.task", opts = {}
28
+ # Check TaskWarrior version, and throw warning if unavailable
29
+ begin
30
+ @version = check_version
31
+ rescue
32
+ warn "Couldn't verify TaskWarrior's version"
33
+ end
28
34
 
29
35
  @data_location = data
30
36
  override_h = DEFAULTS.merge({data_location: data}).merge(opts)
31
37
  @override = Taskrc.new(override_h, :hash)
32
38
  @override_a = override.model_to_rc
33
39
  @taskrc = get_rc
34
-
35
- # Check TaskWarrior version, and throw warning if unavailable
36
- begin
37
- @version = check_version(get_version())
38
- rescue
39
- warn "Couldn't verify TaskWarrior's version"
40
- end
40
+ @udas = get_udas
41
+ add_udas_to_model!(udas) unless udas.nil?
41
42
  end
42
43
 
43
- def check_version version
44
+ def check_version version=nil
45
+ version = get_version if version.nil?
44
46
  if version < LOWEST_VERSION
45
47
  warn "The current TaskWarrior version, #{version}, is untested"
46
48
  end
@@ -3,6 +3,15 @@ require "oj"
3
3
 
4
4
  module Rtasklib
5
5
 
6
+ # Accessed through the main TW, which includes this module, e.g. `tw.all`
7
+ #
8
+ # Ideally should only be the well documented public, user-facing methods.
9
+ # We're getting there.
10
+ #
11
+ # By convention bang methods modify the task database, and non-bang read
12
+ # from the database, e.g. `Controller#all` vs `Controller#modify!`
13
+ #
14
+ # XXX: depends on @override_a currently, which isn't great.
6
15
  module Controller
7
16
  extend self
8
17
 
@@ -12,65 +21,115 @@ module Rtasklib
12
21
  # @api public
13
22
  def all
14
23
  all = []
15
- Execute.task_popen3(*@override_a, "export") do |i, o, e, t|
24
+ Execute.task_popen3(*override_a, "export") do |i, o, e, t|
16
25
  all = MultiJson.load(o.read).map do |x|
17
26
  Rtasklib::Models::TaskModel.new(x)
18
27
  end
19
28
  end
20
- all
29
+ return all
21
30
  end
22
31
 
23
- def filter ids: nil, tags: nil, query: nil
32
+ # Retrieves the current task list filtered by id, tag, or a dom query
33
+ #
34
+ # @example filter by an array of ids
35
+ # tw.some(ids: [1..2, 5])
36
+ # @example filter by tags
37
+ # tw.some(tags: ["+school", "or", "-work"]
38
+ #
39
+ # @param ids [Array<Range, Fixnum, String>, String, Range, Fixnum]
40
+ # @param tags [Array<String>, String]
41
+ # @param dom [Array<String>, String]
42
+ # @return [Array<Models::TaskModel>]
43
+ # @api public
44
+ def some ids: nil, tags: nil, dom: nil
45
+ some = []
46
+ filter_s = Helpers.filter(ids: ids, tags: tags, dom: dom)
47
+ Execute.task_popen3(*@override_a, filter_s, "export") do |i, o, e, t|
48
+ some = MultiJson.load(o.read).map do |x|
49
+ Rtasklib::Models::TaskModel.new(x)
50
+ end
51
+ end
52
+ return some
24
53
  end
25
54
 
26
- def add!
55
+ #
56
+ #
57
+ # @param ids [Array<Range, Fixnum, String>, String, Range, Fixnum]
58
+ # @param tags [Array<String>, String]
59
+ # @param dom [Array<String>, String]
60
+ # @api public
61
+ def add! description
27
62
  end
28
63
 
29
- # def modify! attr:, val:, ids: nil, tags: nil, query: nil
30
- # end
64
+ #
65
+ #
66
+ # @param ids [Array<Range, Fixnum, String>, String, Range, Fixnum]
67
+ # @param tags [Array<String>, String]
68
+ # @param dom [Array<String>, String]
69
+ # @api public
70
+ def modify! attr:, val:, ids: nil, tags: nil, dom: nil
71
+ f = filter(ids, tags, dom)
72
+ query = "#{f} modify #{attr} #{val}"
73
+ Execute.task_popen3(*override_a, query) do |i, o, e, t|
74
+ return t.value
75
+ end
76
+ end
31
77
 
78
+ # Directly call `task undo`, which only applies to edits to the task db
79
+ # not configuration changes
80
+ #
81
+ # @api public
32
82
  def undo!
83
+ Execute.task_popen3(*override_a, "undo") do |i, o, e, t|
84
+ return t.value
85
+ end
33
86
  end
34
87
 
88
+ # Update a configuration variable in the .taskrc
89
+ #
90
+ # @param attr [String]
91
+ # @param val [String]
92
+ # @api public
35
93
  def update_config! attr, val
36
94
  Execute.task_popen3(*override_a, "config #{attr} #{val}") do |i, o, e, t|
37
95
  return t.value
38
96
  end
39
97
  end
40
98
 
41
- # Retrieves an array of hashes with info about the udas currently available
99
+ # Retrieves a hash of hashes with info about the UDAs currently available
100
+ #
101
+ # @return [Hash{Symbol=>Hash}]
102
+ # @api public
42
103
  def get_udas
104
+ udas = {}
43
105
  taskrc.config.attributes
44
- .select { |attr, val| uda_attr? attr }
106
+ .select { |attr, val| Helpers.uda_attr? attr }
45
107
  .sort
46
- .chunk { |attr, val| arbitrary_attr attr }
47
- .map do |attr, arr|
108
+ .chunk { |attr, val| Helpers.arbitrary_attr attr }
109
+ .each do |attr, arr|
48
110
  uda = arr.map do |pair|
49
- key = deep_attr(pair[0])
50
- val = pair[1]
51
- [key, val]
111
+ [Helpers.deep_attr(pair[0]), pair[1]]
52
112
  end
53
- {attr.to_sym => Hash[uda]}
113
+ udas[attr.to_sym] = Hash[uda]
54
114
  end
115
+ return udas
55
116
  end
56
117
 
57
- # Is a given attribute dealing with udas?
58
- def uda_attr? attr
59
- attr.to_s.start_with? "uda"
60
- end
61
- private :uda_attr?
62
-
63
- # Returns part of attribute at a given depth
64
- def arbitrary_attr attr, depth: 1
65
- attr.to_s.split("_")[depth]
66
- end
67
-
68
- # Returns all attribute string after given depth
69
- def deep_attr attr, depth: 2
70
- attr.to_s.split("_")[depth..-1].join("_")
118
+ # Add new found udas to our internal TaskModel
119
+ #
120
+ # @param uda_hash [Hash{Symbol=>Hash}]
121
+ # @param type [Class, nil]
122
+ # @param model [Models::TaskModel, Class]
123
+ # @api protected
124
+ def add_udas_to_model! uda_hash, type=nil, model=Models::TaskModel
125
+ uda_hash.each do |attr, val|
126
+ val.each do |k, v|
127
+ type = Helpers.determine_type(v) if type.nil?
128
+ model.attribute attr, type
129
+ end
130
+ end
71
131
  end
72
- private :deep_attr
73
-
132
+ protected :add_udas_to_model!
74
133
 
75
134
  # Retrieve an array of the uda names
76
135
  #
@@ -95,18 +154,32 @@ module Rtasklib
95
154
  end
96
155
  end
97
156
 
98
- def create_uda name, type: "string", label: nil, values: nil,
157
+ # Add a UDA to the users config/database
158
+ #
159
+ # @param name [String]
160
+ # @param type [String]
161
+ # @param label [String]
162
+ # @param values [String]
163
+ # @param default [String]
164
+ # @param urgency [String]
165
+ # @return [Boolean] success
166
+ # @api public
167
+ def create_uda! name, type: "string", label: nil, values: nil,
99
168
  default: nil, urgency: nil
100
169
  label = name if label.nil?
101
- p name, label, values, default, urgency
102
170
 
103
- update_config "uda.#{name}.type", type
104
- update_config "uda.#{name}.label", label
105
- update_config "uda.#{name}.values", values unless values.nil?
106
- update_config "uda.#{name}.default", default unless default.nil?
107
- update_config "uda.#{name}.urgency", urgency unless urgency.nil?
171
+ update_config("uda.#{name}.type", type)
172
+ update_config("uda.#{name}.label", label)
173
+ update_config("uda.#{name}.values", values) unless values.nil?
174
+ update_config("uda.#{name}.default", default) unless default.nil?
175
+ update_config("uda.#{name}.urgency", urgency) unless urgency.nil?
108
176
  end
109
177
 
178
+ # Calls `task _show` with initial overrides returns a Taskrc object of the
179
+ # result
180
+ #
181
+ # @return [Taskrc]
182
+ # @api public
110
183
  def get_rc
111
184
  res = []
112
185
  Execute.task_popen3(*@override_a, "_show") do |i, o, e, t|
@@ -115,22 +188,16 @@ module Rtasklib
115
188
  Taskrc.new(res, :array)
116
189
  end
117
190
 
191
+ # Calls `task _version` and returns the result
192
+ #
193
+ # @return [String]
194
+ # @api public
118
195
  def get_version
119
196
  version = nil
120
- Execute.task_popen3(*@override_a, "_version") do |i, o, e, t|
121
- version = to_gem_version(o.read.chomp)
197
+ Execute.task_popen3("_version") do |i, o, e, t|
198
+ version = Helpers.to_gem_version(o.read.chomp)
122
199
  end
123
200
  version
124
201
  end
125
-
126
- # Converts a string of format "1.6.2 (adf342jsd)" to Gem::Version object
127
- #
128
- #
129
- def to_gem_version raw
130
- std_ver = raw.chomp.gsub(' ','.').delete('(').delete(')')
131
- Gem::Version.new std_ver
132
- end
133
- private :to_gem_version
134
-
135
202
  end
136
203
  end
@@ -12,12 +12,23 @@ module Rtasklib
12
12
  so \s taskwarrior \s can \s proceed\? \s
13
13
  \(yes/no\)}x }
14
14
 
15
- # popen versions
15
+ # Use Open3#popen3 to execute a unix program with an array of options
16
+ # and an optional block to handle the response.
16
17
  #
18
+ # @example
19
+ # Execute.popen3("task", "export") do |i, o, e, t|
20
+ # # Arbitrary code to handle the response...
21
+ # end
22
+ #
23
+ # @param program [String]
24
+ # @param opts [Array<String>] args to pass directly to the program
25
+ # @param block [Block] to execute after thread is successful
26
+ # @yield [i,o,e,t] STDIN, STDOUT, STDERR, and the thread to that block.
27
+ # @api public
17
28
  def popen3 program='task', *opts, &block
18
29
  execute = opts.unshift(program)
19
30
  execute = execute.join(" ")
20
- p execute
31
+ warn execute
21
32
 
22
33
  Open3.popen3(execute) do |i, o, e, t|
23
34
  handle_response(e, t)
@@ -25,10 +36,30 @@ module Rtasklib
25
36
  end
26
37
  end
27
38
 
39
+ # Same as Execute#popen3, only defaults to using the 'task' program for
40
+ # convenience.
41
+ #
42
+ # @example
43
+ # Execute.task_popen3("export") do |i, o, e, t|
44
+ # # Arbitrary code to handle the response...
45
+ # end
46
+ #
47
+ # @param opts [Array<String>] args to pass directly to the program
48
+ # @param block [Block] to execute after thread is successful
49
+ # @yield [i,o,e,t] STDIN, STDOUT, STDERR, and the thread to that block.
50
+ # @api public
28
51
  def task_popen3 *opts, &block
29
52
  popen3('task', opts, &block)
30
53
  end
31
54
 
55
+ # Same as Execute#popen3, but yields each line of input
56
+ #
57
+ # @param program [String]
58
+ # @param opts [Array<String>] args to pass directly to the program
59
+ # @param block [Block] to execute after thread is successful
60
+ # @yield [l,i,o,e,t] a line of STDIN, STDIN, STDOUT, STDERR,
61
+ # and the thread to that block.
62
+ # @api public
32
63
  def each_popen3 program='task', *opts, &block
33
64
  popen3(program, *opts) do |i, o, e, t|
34
65
  o.each_line do |l|
@@ -37,49 +68,28 @@ module Rtasklib
37
68
  end
38
69
  end
39
70
 
71
+ # Same as Execute#each_popen3, but calls it with the 'task' program
72
+ #
73
+ # @param opts [Array<String>] args to pass directly to the program
74
+ # @param block [Block] to execute after thread is successful
75
+ # @yield [l,i,o,e,t] a line of STDIN, STDIN, STDOUT, STDERR,
76
+ # and the thread to that block.
77
+ # @api public
40
78
  def task_each_popen3 *opts, &block
41
- popen3(program, *opts) do |i, o, e, t|
42
- yield(i, o, e, t)
79
+ each_popen3("task", *opts) do |l, i, o, e, t|
80
+ yield(l, i, o, e, t)
43
81
  end
44
82
  end
45
83
 
84
+ # Default error handling called in every popen3 call. Only executes if
85
+ # thread had a failing exit code
86
+ #
87
+ # @raise [RuntimeError] if failing exit code
46
88
  def handle_response stderr, thread
47
89
  unless thread.value.success?
48
90
  puts stderr.read
49
- exit(-1)
91
+ raise thread.inspect
50
92
  end
51
93
  end
52
-
53
- # Non-greedy json object detection
54
- # if /\{.*\}/ =~ l
55
- # p l.chomp
56
- # res.push(l.chomp)
57
- # end
58
- # def task create_new, *opts, &block
59
- # exp_regex = @@exp_regex
60
- # retval = 0
61
- # res = nil
62
- # buff = ""
63
- #
64
- # run("task", *opts) do |exp, procedure|
65
- # res = procedure.any do
66
- # puts exp
67
- # expect exp_regex[:create_rc] do
68
- # if create_new
69
- # send "yes"
70
- # else
71
- # send "no"
72
- # end
73
- # end
74
- # block.call if block_given?
75
- # end
76
-
77
- # Filters should be a list of values
78
- # Ranges interpreted as ids
79
- # 1...5 : "1-5"
80
- # 1..5 : "1-4"
81
- # 1 : "1"
82
- # and joined with ","
83
- # [1...5, 8, 9] : "1-5,8,9"
84
94
  end
85
95
  end
@@ -0,0 +1,205 @@
1
+ require "multi_json"
2
+ require "oj"
3
+
4
+ module Rtasklib
5
+
6
+ # A collection of stateless, non-end-user facing functions available
7
+ # throughout the library
8
+ module Helpers
9
+ # make this module a stateless, singleton
10
+ extend self
11
+
12
+ # Converts ids, tags, and dom queries to a single string ready to pass
13
+ # directly to task.
14
+ #
15
+ # @param ids[Range, Array<String, Range, Fixnum>, String, Fixnum]
16
+ # @param tags[String, Array<String>]
17
+ # @param dom[String, Array<String>]
18
+ # @return [String] a string with ids tags and dom joined by a space
19
+ # @api public
20
+ def filter ids: nil, tags: nil, dom: nil
21
+ id_s = tag_s = dom_s = ""
22
+ id_s = process_ids(ids) unless ids.nil?
23
+ tag_s = process_tags(tags) unless tags.nil?
24
+ dom_s = process_dom(dom) unless dom.nil?
25
+ return "#{id_s} #{tag_s} #{dom_s}".strip
26
+ end
27
+
28
+ # Filters should be a list of values
29
+ # Ranges interpreted as ids
30
+ # 1...5 : "1,2,3,4,5"
31
+ # 1..5 : "1,2,3,4"
32
+ # 1 : "1"
33
+ # and joined with ","
34
+ # [1...5, 8, 9] : "1,2,3,4,5,8,9"
35
+ #
36
+ # @param id_a [Array<String, Range, Fixnum>]
37
+ # @return [String]
38
+ # @api public
39
+ def id_a_to_s id_a
40
+ id_a.map do |el|
41
+ proc_ids = process_ids(el)
42
+ proc_ids
43
+ end.compact.join(",")
44
+ end
45
+
46
+ # Converts arbitrary id input to a task safe string
47
+ #
48
+ # @param ids[Range, Array<String, Range, Fixnum>, String, Fixnum]
49
+ # @api public
50
+ def process_ids ids
51
+ case ids
52
+ when Range
53
+ return id_range_to_s(ids)
54
+ when Array
55
+ return id_a_to_s(ids)
56
+ when String
57
+ return ids.delete(" ")
58
+ when Fixnum
59
+ return ids
60
+ end
61
+ end
62
+
63
+ # Convert a range to a comma separated strings, e.g. 1..4 -> "1,2,3,4"
64
+ #
65
+ # @param id_range [Range]
66
+ # @return [Array<String>]
67
+ # @api public
68
+ def id_range_to_s id_range
69
+ id_range.to_a.join(",")
70
+ end
71
+
72
+ # Convert a tag string or an array of strings to a space separated string
73
+ #
74
+ # @param tags [String, Array<String>]
75
+ # @api private
76
+ def process_tags tags
77
+ case tags
78
+ when String
79
+ tags.split(" ").map { |t| process_tag t }.join(" ")
80
+ when Array
81
+ tags.map { |t| process_tags t }.join(" ")
82
+ end
83
+ end
84
+
85
+ # Ensures that a tag begins with a + or -
86
+ #
87
+ # @return [String]
88
+ # @api public
89
+ def process_tag tag
90
+ reserved_symbols = %w{+ - and or xor < <= = != >= > ( )}
91
+
92
+ # convert plain tags to plus tags
93
+ unless tag.start_with?(*reserved_symbols)
94
+ tag = "+#{tag}"
95
+ end
96
+ return tag
97
+ end
98
+
99
+ # @api private
100
+ def process_dom dom
101
+ end
102
+
103
+ # Is a given taskrc attribute dealing with udas?
104
+ #
105
+ # @api public
106
+ def uda_attr? attr
107
+ attr.to_s.start_with? "uda"
108
+ end
109
+
110
+ # Returns part of attribute at a given depth
111
+ #
112
+ # @api public
113
+ def arbitrary_attr attr, depth: 1
114
+ attr.to_s.split("_")[depth]
115
+ end
116
+
117
+ # Returns all attribute string after given depth
118
+ #
119
+ # @api public
120
+ def deep_attr attr, depth: 2
121
+ attr.to_s.split("_")[depth..-1].join("_")
122
+ end
123
+
124
+ # Converts a string of format "1.6.2 (adf342jsd)" to Gem::Version object
125
+ #
126
+ # @param raw [String]
127
+ # @return [Gem::Version]
128
+ # @api public
129
+ def to_gem_version raw
130
+ std_ver = raw.chomp.gsub(' ','.').delete('(').delete(')')
131
+ Gem::Version.new std_ver
132
+ end
133
+
134
+ # Determine the type that a value should be coerced to
135
+ # Int needs to precede float because ints are also floats
136
+ # Doesn't detect arrays, b/c task stores these as comma separated strings
137
+ # which could just as easily be Strings....
138
+ # If nothing works it defaults to String.
139
+ # TODO: JSON parse
140
+ #
141
+ # @param value [Object] anything that needs to be coerced, probably string
142
+ # @return [Axiom::Types::Boolean, Integer, Float, String]
143
+ # @api public
144
+ def determine_type value
145
+ if boolean? value
146
+ return Axiom::Types::Boolean
147
+ elsif integer? value
148
+ return Integer
149
+ elsif float? value
150
+ return Float
151
+ elsif json? value
152
+ return MultiJson
153
+ else
154
+ return String
155
+ end
156
+ end
157
+
158
+ # Can the input be coerced to an integer without losing information?
159
+ #
160
+ # @return [Boolean] true if coercible, false if not
161
+ # @api private
162
+ def integer? value
163
+ value.to_i.to_s == value rescue false
164
+ end
165
+ private :integer?
166
+
167
+ # Can the input be coerced to a JSON object without losing information?
168
+ #
169
+ # @return [Boolean] true if coercible, false if not
170
+ # @api private
171
+ def json? value
172
+ begin
173
+ return false unless value.is_a? String
174
+ MultiJson.load(value)
175
+ true
176
+ rescue MultiJson::ParseError
177
+ false
178
+ end
179
+ end
180
+ private :json?
181
+
182
+ # Can the input be coerced to a float without losing information?
183
+ #
184
+ # @return [Boolean] true if coercible, false if not
185
+ # @api private
186
+ def float? value
187
+ begin
188
+ true if Float(value)
189
+ rescue
190
+ false
191
+ end
192
+ end
193
+ private :float?
194
+
195
+ # Can the input be coerced to a boolean without losing information?
196
+ #
197
+ # @return [Boolean] true if coercible, false if not
198
+ # @api private
199
+ def boolean? value
200
+ ["on", "off", "yes", "no", "false", "true"]
201
+ .include? value.to_s.downcase rescue false
202
+ end
203
+ private :boolean?
204
+ end
205
+ end
@@ -1,22 +1,37 @@
1
1
  require "virtus"
2
2
  require "active_model"
3
+ require 'iso8601'
4
+ require 'date'
3
5
 
4
6
  module Rtasklib::Models
5
- ValidationError = Class.new RuntimeError
6
7
 
7
- class UUID < Virtus::Attribute
8
- def coerce(value)
9
- value.to_s
8
+ # A subclass of the ISO8601::Duration object that use `task calc` to parse
9
+ # string names like 'weekly', 'biannual', and '3 quarters'
10
+ #
11
+ # Modifies the #initialize method, preserving the original string duration
12
+ class TWDuration < ISO8601::Duration
13
+ attr_reader :frozen_value
14
+
15
+ def initialize input, base=nil
16
+ @frozen_value = input.dup.freeze
17
+ parsed = `task calc #{input}`.chomp
18
+
19
+ super parsed, base
20
+ end
21
+ end
22
+
23
+ # Custom coercer to change a string input into an TWDuration object
24
+ class VirtusDuration < Virtus::Attribute
25
+ def coerce(v)
26
+ if v.nil? || v.blank? then nil else TWDuration.new(v) end
10
27
  end
11
28
  end
12
29
 
13
30
  RcBooleans = Virtus.model do |mod|
14
31
  mod.coerce = true
15
32
  mod.coercer.config.string.boolean_map = {
16
- 'no' => false,
17
- 'yes' => true,
18
- 'on' => true,
19
- 'off' => false }
33
+ 'yes' => true, 'on' => true,
34
+ 'no' => false, 'off' => false }
20
35
  end
21
36
 
22
37
  class TaskrcModel
@@ -61,7 +76,7 @@ module Rtasklib::Models
61
76
  attribute :wait, DateTime
62
77
 
63
78
  # Required only for tasks that are Recurring or have Recurring Parent
64
- attribute :recur, DateTime
79
+ attribute :recur, VirtusDuration
65
80
 
66
81
  # Optional except for tasks with Recurring Parents
67
82
  attribute :due, DateTime
@@ -74,8 +89,6 @@ module Rtasklib::Models
74
89
  attribute :imask, String
75
90
  attribute :modified, DateTime
76
91
 
77
- # TODO: handle arbitrary UDA's
78
-
79
92
  # Refactoring idea, need to understand Virtus internals a bit better
80
93
  # [:mask, :imask, :modified, :status, :uuid, :entry].each do |ro_attr|
81
94
  # define_method("set_#{ro_attr.to_s}") do |value|
@@ -25,7 +25,7 @@ module Rtasklib
25
25
  hash_to_model(rc)
26
26
  when :path
27
27
  if path_exist?(rc)
28
- mappable_to_model(File.open(rc))
28
+ mappable_to_model(File.open(rc).readlines)
29
29
  else
30
30
  raise RuntimeError.new("rc path does not exist on the file system")
31
31
  end
@@ -53,7 +53,7 @@ module Rtasklib
53
53
  # Converts a .taskrc file path into a Hash that can be converted into a
54
54
  # TaskrcModel object
55
55
  #
56
- # @param rc_path [String,Pathname] a valid pathname to a .taskrc file
56
+ # @param rc_file [String,Pathname] a valid pathname to a .taskrc file
57
57
  # @return [Models::TaskrcModel] the instance variable config
58
58
  # @api private
59
59
  def mappable_to_model rc_file
@@ -123,7 +123,6 @@ module Rtasklib
123
123
 
124
124
  # Dynamically add a Virtus attr, detect Boolean, Integer, and Float types
125
125
  # based on the value, otherwise just treat it like a string.
126
- # Int needs to precede float because ints are also floats
127
126
  # Modifies the config instance variable
128
127
  # TODO: May also be able to detect arrays
129
128
  #
@@ -132,22 +131,14 @@ module Rtasklib
132
131
  # @return [undefined]
133
132
  # @api private
134
133
  def add_model_attr attr, value
135
- if boolean? value
136
- config.attribute attr.to_sym, Axiom::Types::Boolean
137
- elsif integer? value
138
- config.attribute attr.to_sym, Integer
139
- elsif float? value
140
- config.attribute attr.to_sym, Float
141
- else
142
- config.attribute attr.to_sym, String
143
- end
134
+ config.attribute(attr.to_sym, Helpers.determine_type(value))
144
135
  end
145
136
  private :add_model_attr
146
137
 
147
138
  # Modifies the value of a given attr in the config object
148
139
  #
149
140
  # @param attr [#to_s] the name for the attr, e.g. "json_array"
150
- # @param attr [String] the value of the attr, e.g. "yes"
141
+ # @param value [String] the value of the attr, e.g. "yes"
151
142
  # @return [undefined]
152
143
  # @api public
153
144
  def set_model_attr_value attr, value
@@ -194,20 +185,5 @@ module Rtasklib
194
185
  end
195
186
  end
196
187
  private :path_exist?
197
-
198
- def integer? value
199
- value.to_i.to_s == value
200
- end
201
- private :integer?
202
-
203
- def float? value
204
- Float(value) rescue false
205
- end
206
- private :float?
207
-
208
- def boolean? value
209
- ["on", "off", "yes", "no", "false", "true"].include? value.to_s.downcase
210
- end
211
- private :boolean?
212
188
  end
213
189
  end
@@ -1,3 +1,3 @@
1
1
  module Rtasklib
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.5"
3
3
  end
@@ -3,16 +3,62 @@ require 'spec_helper'
3
3
  describe Rtasklib::Controller do
4
4
  include Rtasklib::Controller
5
5
 
6
- it 'can handle sub version numbers' do
7
- expect(to_gem_version("2.5.3 (afdj5)"))
8
- .to eq(Gem::Version.new("2.5.3.afdj5"))
6
+ shared_examples_for 'export' do
7
+ it 'should return an Array' do
8
+ expect(subject.class).to eq Array
9
+ end
10
+
11
+ it 'should return an Array<TaskModel>' do
12
+ expect(subject.each do |t|
13
+ expect(t.class).to eq(Rtasklib::Models::TaskModel)
14
+ end)
15
+ end
16
+ end
17
+
18
+ shared_examples_for 'export all' do
19
+ it_behaves_like 'export'
20
+
21
+ it 'should load in the correct number of task models' do
22
+ expect(subject.size).to eq(4)
23
+ end
9
24
  end
10
25
 
11
26
  describe 'Rtasklib::Controller#all' do
27
+ subject { Rtasklib::TaskWarrior.new("spec/data/.task").all }
28
+ it_behaves_like 'export all'
29
+ end
30
+
31
+ describe 'Rtasklib::Controller#some' do
32
+
33
+ describe '#some without arguments should behave like #all' do
34
+ subject { Rtasklib::TaskWarrior.new("spec/data/.task").some }
35
+ it_behaves_like 'export all'
36
+ end
37
+
38
+ describe '#some should accept filter parameters' do
39
+ subject { Rtasklib::TaskWarrior.new("spec/data/.task").some ids:[1,2] }
40
+ it_behaves_like 'export'
41
+
42
+ it 'should load in the correct number of task models' do
43
+ expect(subject.size).to eq(2)
44
+ end
45
+ end
12
46
  end
13
47
 
14
48
  describe 'Rtasklib::Controller#check_uda' do
15
49
  it 'should return true if a uda is found that matches the input' do
16
50
  end
17
51
  end
52
+
53
+ describe 'Rtasklib::Controller#all' do
54
+ end
55
+
56
+ describe 'Rtasklib::Controller#add!' do
57
+ end
58
+
59
+ describe 'Rtasklib::Controller#modify!' do
60
+ end
61
+
62
+ describe 'Rtasklib::Controller#undo!' do
63
+ end
18
64
  end
@@ -1,2 +1,5 @@
1
1
  {"description":"Wash dishes","entry":"20150316T174823Z","modified":"20150316T174823Z","status":"pending","uuid":"1ecac94d-b4d6-4c02-98f7-75e4de03006b"}
2
2
  {"description":"Clean room","entry":"20150316T174852Z","modified":"20150316T174852Z","status":"pending","uuid":"12d3176a-b6b1-42ab-90ad-6cd6acf8d6a1"}
3
+ {"description":"Test recur","entry":"20150512T200658Z","modified":"20150512T200658Z","status":"pending","uuid":"d203a656-f7ab-4707-8e38-abdced29acc4"}
4
+ {"description":"Test recur","due":"20160101T045959Z","entry":"20150512T200658Z","modified":"20150512T200658Z","recur":"weekly","status":"recurring","uuid":"d203a656-f7ab-4707-8e38-abdced29acc4"}
5
+ {"description":"biannual test","due":"20160101T045959Z","entry":"20150512T204148Z","modified":"20150512T204148Z","recur":"biannual","status":"recurring","uuid":"45fc918e-f106-45d8-96b6-37eaf61f2f4e"}
@@ -1,2 +1,4 @@
1
1
  [description:"Wash dishes" entry:"1426528103" modified:"1426528103" status:"pending" uuid:"1ecac94d-b4d6-4c02-98f7-75e4de03006b"]
2
2
  [description:"Clean room" entry:"1426528132" modified:"1426528132" status:"pending" uuid:"12d3176a-b6b1-42ab-90ad-6cd6acf8d6a1"]
3
+ [description:"Test recur" due:"1451624399" entry:"1431461218" modified:"1431461218" recur:"weekly" status:"recurring" uuid:"d203a656-f7ab-4707-8e38-abdced29acc4"]
4
+ [description:"biannual test" due:"1451624399" entry:"1431463308" modified:"1431463308" recur:"biannual" status:"recurring" uuid:"45fc918e-f106-45d8-96b6-37eaf61f2f4e"]
@@ -4,3 +4,13 @@ new [description:"Wash dishes" entry:"1426528103" modified:"1426528103" status:"
4
4
  time 1426528132
5
5
  new [description:"Clean room" entry:"1426528132" modified:"1426528132" status:"pending" uuid:"12d3176a-b6b1-42ab-90ad-6cd6acf8d6a1"]
6
6
  ---
7
+ time 1431461218
8
+ new [description:"Test recur" entry:"1431461218" modified:"1431461218" status:"pending" uuid:"d203a656-f7ab-4707-8e38-abdced29acc4"]
9
+ ---
10
+ time 1431461251
11
+ old [description:"Test recur" entry:"1431461218" modified:"1431461218" status:"pending" uuid:"d203a656-f7ab-4707-8e38-abdced29acc4"]
12
+ new [description:"Test recur" due:"1451624399" entry:"1431461218" modified:"1431461218" recur:"weekly" status:"recurring" uuid:"d203a656-f7ab-4707-8e38-abdced29acc4"]
13
+ ---
14
+ time 1431463308
15
+ new [description:"biannual test" due:"1451624399" entry:"1431463308" modified:"1431463308" recur:"biannual" status:"recurring" uuid:"45fc918e-f106-45d8-96b6-37eaf61f2f4e"]
16
+ ---
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rtasklib::Helpers do
4
+
5
+ describe 'Rtasklib::Helpers#to_gem_version' do
6
+
7
+ it 'can handle sub version numbers' do
8
+ expect(Rtasklib::Helpers.to_gem_version("2.5.3 (afdj5)"))
9
+ .to eq(Gem::Version.new("2.5.3.afdj5"))
10
+ end
11
+ end
12
+
13
+ describe 'Rtasklib::Helpers#determine_type' do
14
+
15
+ it 'considers "10" an integer' do
16
+ expect(Rtasklib::Helpers.determine_type("10")).to eq(Integer)
17
+ end
18
+
19
+ it 'considers "10.1" a float' do
20
+ expect(Rtasklib::Helpers.determine_type("10.1")).to eq(Float)
21
+ end
22
+
23
+ it 'considers "on" a boolean' do
24
+ expect(Rtasklib::Helpers.determine_type("on")).to eq(Axiom::Types::Boolean)
25
+ end
26
+
27
+ it 'considers "{"yolo":[1,2,3]}" a JSON object' do
28
+ test_json = '{"id":1,"description":"Anonymous Book",
29
+ "entry":"20150115T190114Z","modified":"20150115T190114Z",
30
+ "project":"Read","status":"pending","tags":["stuff"],
31
+ "uuid":"c483b58d-a3f2-4a2a-b944-8b41414309cb",
32
+ "urgency":"2.45753"}'
33
+ expect(Rtasklib::Helpers.determine_type(test_json)).to eq(MultiJson)
34
+ end
35
+
36
+ it 'considers "on off" a String' do
37
+ expect(Rtasklib::Helpers.determine_type("on ")).to eq(String)
38
+ end
39
+ end
40
+
41
+ describe 'Rtasklib::Helpers#filter' do
42
+
43
+ it 'treats arrays of ranges properly' do
44
+ expect(Rtasklib::Helpers.filter(ids: [1..3,5...6])).to eq("1,2,3,5")
45
+ end
46
+
47
+ it 'treats arrays of strings properly' do
48
+ expect(Rtasklib::Helpers.filter(ids: ["1,2", "5"])).to eq("1,2,5")
49
+ end
50
+
51
+ it 'treats arrays of ints properly' do
52
+ expect(Rtasklib::Helpers.filter(ids: [1,2,3,4,5])).to eq("1,2,3,4,5")
53
+ end
54
+
55
+ it 'treats arrays mixed objects properly' do
56
+ expect(Rtasklib::Helpers.filter(ids: [1,2,3,4,5, 10..20, "7,8"]))
57
+ .to eq("1,2,3,4,5,10,11,12,13,14,15,16,17,18,19,20,7,8")
58
+ end
59
+
60
+ it 'treats tag arrays properly' do
61
+ expect(Rtasklib::Helpers.filter(tags: ["(", "+stuff", "or", "-school", ")", "work"]))
62
+ .to eq("( +stuff or -school ) +work")
63
+ end
64
+
65
+ it 'treats tag strings properly' do
66
+ expect(Rtasklib::Helpers.filter(tags: "+stuff -school work"))
67
+ .to eq("+stuff -school +work")
68
+ end
69
+
70
+ it 'treats tag/id combos properly' do
71
+ expect(Rtasklib::Helpers.filter(ids: [1,2,4..5], tags: "+stuff -school work"))
72
+ .to eq("1,2,4,5 +stuff -school +work")
73
+ end
74
+
75
+ end
76
+ end
@@ -6,8 +6,8 @@ describe Rtasklib do
6
6
  end
7
7
 
8
8
  describe Rtasklib::TaskWarrior do
9
- describe "Rtasklib::TaskWarrior.new('spec/data/.taskrc')" do
10
- subject{ Rtasklib::TaskWarrior.new('spec/data/.taskrc') }
9
+ describe "Rtasklib::TaskWarrior.new('spec/data/.task')" do
10
+ subject{ Rtasklib::TaskWarrior.new('spec/data/.task') }
11
11
  it 'has a version number' do
12
12
  expect(subject.version.class).to eq Gem::Version
13
13
  end
@@ -16,8 +16,8 @@ describe Rtasklib do
16
16
  end
17
17
  end
18
18
 
19
- describe "Rtasklib::TaskWarrior.new('spec/data/.taskrc', {color: off})" do
20
- subject{ Rtasklib::TaskWarrior.new('spec/data/.taskrc', {verbose: 'on'}) }
19
+ describe "Rtasklib::TaskWarrior.new('spec/data/.task', {color: off})" do
20
+ subject{ Rtasklib::TaskWarrior.new('spec/data/.task', {verbose: 'on'}) }
21
21
  it 'updates the default configuration override' do
22
22
  end
23
23
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rtasklib
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Will Paul
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-05-12 00:00:00.000000000 Z
11
+ date: 2015-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: virtus
@@ -245,8 +245,8 @@ files:
245
245
  - lib/rtasklib.rb
246
246
  - lib/rtasklib/controller.rb
247
247
  - lib/rtasklib/execute.rb
248
+ - lib/rtasklib/helpers.rb
248
249
  - lib/rtasklib/models.rb
249
- - lib/rtasklib/serializer.rb
250
250
  - lib/rtasklib/taskrc.rb
251
251
  - lib/rtasklib/version.rb
252
252
  - rtasklib.gemspec
@@ -257,9 +257,9 @@ files:
257
257
  - spec/data/.task/undo.data
258
258
  - spec/data/.taskrc
259
259
  - spec/execute_spec.rb
260
+ - spec/helpers_spec.rb
260
261
  - spec/models_spec.rb
261
262
  - spec/rtasklib_spec.rb
262
- - spec/serializer_spec.rb
263
263
  - spec/spec_helper.rb
264
264
  - spec/taskrc_spec.rb
265
265
  homepage: http://github.com/dropofwill/rtasklib
@@ -283,7 +283,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
283
283
  requirements:
284
284
  - taskwarrior, >=2.4.0
285
285
  rubyforge_project:
286
- rubygems_version: 2.4.5
286
+ rubygems_version: 2.2.2
287
287
  signing_key:
288
288
  specification_version: 4
289
289
  summary: A Ruby wrapper around the TaskWarrior CLI
@@ -1,10 +0,0 @@
1
- require "active_model"
2
- require "active_model/serializer"
3
-
4
- module Rtasklib
5
-
6
- module Models
7
- class TaskSerializer < ActiveModel::Serializer
8
- end
9
- end
10
- end
File without changes