todo.txt 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +7 -0
  3. data/Gemfile.lock +29 -0
  4. data/MIT-LICENSE +21 -0
  5. data/NOTES.md +0 -0
  6. data/README.md +11 -0
  7. data/lib/todo.rb +20 -0
  8. data/lib/todo/cli.rb +39 -0
  9. data/lib/todo/cli/archive.rb +39 -0
  10. data/lib/todo/cli/cmd.rb +26 -0
  11. data/lib/todo/cli/list.rb +42 -0
  12. data/lib/todo/cli/push.rb +42 -0
  13. data/lib/todo/cli/toggle.rb +22 -0
  14. data/lib/todo/data/item.rb +58 -0
  15. data/lib/todo/data/list.rb +64 -0
  16. data/lib/todo/data/matcher.rb +42 -0
  17. data/lib/todo/data/parser.rb +41 -0
  18. data/lib/todo/helpers/hash/compact.rb +13 -0
  19. data/lib/todo/helpers/hash/format.rb +13 -0
  20. data/lib/todo/helpers/hash/slice.rb +17 -0
  21. data/lib/todo/helpers/object/presence.rb +26 -0
  22. data/lib/todo/helpers/string/camelize.rb +16 -0
  23. data/lib/todo/src/file.rb +24 -0
  24. data/lib/todo/src/idonethis.rb +52 -0
  25. data/lib/todo/src/io.rb +29 -0
  26. data/lib/todo/support/http.rb +31 -0
  27. data/lib/todo/support/options_parser.rb +31 -0
  28. data/lib/todo/version.rb +3 -0
  29. data/lib/todo/view.rb +49 -0
  30. data/spec/spec_helper.rb +9 -0
  31. data/spec/support/io.rb +9 -0
  32. data/spec/todo/cli/archive_spec.rb +16 -0
  33. data/spec/todo/cli/list_spec.rb +13 -0
  34. data/spec/todo/cli/toggle_spec.rb +25 -0
  35. data/spec/todo/data/item_spec.rb +94 -0
  36. data/spec/todo/data/list_spec.rb +59 -0
  37. data/spec/todo/data/parser_spec.rb +27 -0
  38. data/spec/todo/helpers/hash/format_spec.rb +9 -0
  39. data/spec/todo/helpers/hash/slice_spec.rb +8 -0
  40. data/spec/todo/helpers/object/presence_spec.rb +42 -0
  41. data/spec/todo/helpers/string/string_spec.rb +7 -0
  42. data/spec/todo/src/file_spec.rb +33 -0
  43. data/spec/todo/src/io_spec.rb +18 -0
  44. data/spec/todo/view_spec.rb +17 -0
  45. data/todo.gemspec +20 -0
  46. data/todo.md +10 -0
  47. metadata +89 -0
@@ -0,0 +1,42 @@
1
+ module Todo
2
+ module Data
3
+ class Matcher < Struct.new(:item, :data)
4
+ def matches?
5
+ return true if data.empty?
6
+ data[:id] ? match_id : match_data
7
+ end
8
+
9
+ private
10
+
11
+ def match_data
12
+ data.keys.inject(true) do |result, key|
13
+ result && send(:"match_#{key}")
14
+ end
15
+ end
16
+
17
+ def match_id
18
+ item.id.to_i == data[:id].to_i
19
+ end
20
+
21
+ def match_text
22
+ item.text.include?(data[:text])
23
+ end
24
+
25
+ def match_status
26
+ item.status && item.status.to_sym == normalize_status(data[:status].to_sym)
27
+ end
28
+
29
+ def match_since
30
+ item.done_date.to_s >= data[:since].to_s
31
+ end
32
+
33
+ def match_before
34
+ item.done_date.to_s < data[:before].to_s
35
+ end
36
+
37
+ def normalize_status(status)
38
+ status == :pending ? :pend : status
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ require 'todo'
2
+ require 'todo/helpers/hash/compact'
3
+
4
+ module Todo
5
+ module Data
6
+ class Parser < Struct.new(:line)
7
+ STATUS = /^(#{STATUSES.values.join('|')}){1}\s*/
8
+ ID = /\s*\[(\d+)\]/
9
+ TAG = /\s*([^\s]+):([^\s]+)/
10
+
11
+ include Helpers::Hash::Compact
12
+
13
+ def parse
14
+ compact(
15
+ id: id,
16
+ text: text,
17
+ status: status,
18
+ tags: tags,
19
+ )
20
+ end
21
+
22
+ private
23
+
24
+ def status
25
+ STATUSES.invert[line.match(STATUS) && $1]
26
+ end
27
+
28
+ def id
29
+ line =~ ID && $1.to_i
30
+ end
31
+
32
+ def text
33
+ line.sub(STATUS, '').gsub(ID, '').gsub(TAG, '').rstrip
34
+ end
35
+
36
+ def tags
37
+ Hash[line.scan(TAG).map { |key, value| [key.to_sym, value] }]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ module Todo
2
+ module Helpers
3
+ module Hash
4
+ module Compact
5
+ def compact(hash)
6
+ hash.reject { |key, value| value.nil? }
7
+ end
8
+
9
+ extend self
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Todo
2
+ module Helpers
3
+ module Hash
4
+ module Format
5
+ def to_pairs(hash, separator = '=')
6
+ hash.map { |key, value| [key, value].join(separator) }
7
+ end
8
+
9
+ extend self
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module Todo
2
+ module Helpers
3
+ module Hash
4
+ module Slice
5
+ def slice(hash, *keys)
6
+ hash.select { |key, value| keys.include?(key) }
7
+ end
8
+
9
+ def except(hash, *keys)
10
+ hash.reject { |key, value| keys.include?(key) }
11
+ end
12
+
13
+ extend self
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ module Todo
2
+ module Helpers
3
+ module Object
4
+ module Presence
5
+ def present?(obj)
6
+ case obj
7
+ when NilClass, FalseClass
8
+ false
9
+ when TrueClass, Symbol, Numeric
10
+ true
11
+ when ::String, ::Array, ::Hash
12
+ !obj.empty?
13
+ else
14
+ fail "Unexpected object type #{obj.inspect}"
15
+ end
16
+ end
17
+
18
+ def blank?(obj)
19
+ not present?(obj)
20
+ end
21
+
22
+ extend self
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ module Todo
2
+ module Helpers
3
+ module String
4
+ module Camelize
5
+ def camelize(string)
6
+ string.to_s.
7
+ sub(/^[a-z\d]*/) { $&.capitalize }.
8
+ gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }.
9
+ gsub('/', '::')
10
+ end
11
+
12
+ extend self
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ module Todo
2
+ module Src
3
+ class File
4
+ attr_reader :path, :mode
5
+
6
+ def initialize(path, opts = {})
7
+ @path = path
8
+ @mode = opts[:mode] || 'w+'
9
+ end
10
+
11
+ def exists?
12
+ ::File.exists?(path)
13
+ end
14
+
15
+ def read
16
+ exists? ? ::File.readlines(path).map(&:rstrip) : []
17
+ end
18
+
19
+ def write(lines)
20
+ ::File.open(path, mode) { |f| f.puts(lines.join("\n")) }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,52 @@
1
+ require 'json'
2
+ require 'todo/support/http'
3
+
4
+ module Todo
5
+ module Src
6
+ class Idonthis < Struct.new(:config, :opts)
7
+ URL = 'https://idonethis.com/api/v0.1/dones/'
8
+
9
+ def include?(item)
10
+ texts.include?(item.text) or ids.include?(item.id)
11
+ end
12
+
13
+ def write(lines)
14
+ lines.each { |line| post(line) }
15
+ end
16
+
17
+ private
18
+
19
+ def texts
20
+ items.map(&:text)
21
+ end
22
+
23
+ def ids
24
+ items.map(&:id).compact
25
+ end
26
+
27
+ def items
28
+ @items ||= fetch.map { |item| Item.new(nil, item['raw_text']) }
29
+ end
30
+
31
+ def fetch
32
+ JSON.parse(get.body)['results'] || []
33
+ end
34
+
35
+ def get
36
+ Http.new(URL, headers: headers, params: params).get
37
+ end
38
+
39
+ def post(text)
40
+ Http.new(URL, headers: headers).post(raw_text: text, team: config[:team])
41
+ end
42
+
43
+ def headers
44
+ { 'Authorization' => "Token #{config[:token]}" }
45
+ end
46
+
47
+ def params
48
+ { owner: config[:username], team: config[:team], done_date_after: opts[:since], page_size: 100 }
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ module Todo
2
+ module Src
3
+ class Io
4
+ attr_reader :opts
5
+
6
+ def initialize(opts = {})
7
+ @opts = opts
8
+ end
9
+
10
+ def read
11
+ @lines ||= input.readlines.map(&:rstrip)
12
+ end
13
+
14
+ def write(lines)
15
+ output.puts(lines.join("\n"))
16
+ end
17
+
18
+ private
19
+
20
+ def input
21
+ opts[:in] || $stdin
22
+ end
23
+
24
+ def output
25
+ opts[:out] || $stdout
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+ module Todo
5
+ module Support
6
+ class Http
7
+ attr_reader :uri, :headers
8
+
9
+ def initialize(url, opts)
10
+ @uri = URI(url).tap { |url| url.query = URI.encode_www_form(opts[:params] || {}) }
11
+ @headers = opts[:headers] || {}
12
+ end
13
+
14
+ def get
15
+ client = Net::HTTP.new(url.host, url.port)
16
+ client.use_ssl = true
17
+ response = client.get(uri, headers)
18
+ raise "Failed to get #{url.to_s}" unless response.code.to_s[0] == '2'
19
+ response
20
+ end
21
+
22
+ def post(data)
23
+ request = Net::HTTP::Post.new(url, headers)
24
+ request.form_data = data
25
+ response = http.request(request)
26
+ raise "Failed to post to #{url.to_s}: #{response.body}" unless response.code.to_s[0] == '2'
27
+ response
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ require 'optparse'
2
+
3
+ module Todo
4
+ module Support
5
+ module OptionsParser
6
+ def opt(*args, &block)
7
+ rules << [args, block]
8
+ end
9
+
10
+ def parse(args)
11
+ opts = {}
12
+ args = parser(opts).parse(args)
13
+ [args, opts]
14
+ end
15
+
16
+ private
17
+
18
+ def rules
19
+ @rules ||= (superclass.instance_variable_get(:@rules) || []).dup
20
+ end
21
+
22
+ def parser(opts)
23
+ OptionParser.new do |parser|
24
+ rules.each do |args, block|
25
+ parser.on(*args) { |value| block.call(opts, value.strip) }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ module Todo
2
+ VERSION = '0.0.3'
3
+ end
data/lib/todo/view.rb ADDED
@@ -0,0 +1,49 @@
1
+ require 'todo'
2
+ require 'todo/helpers/hash/format'
3
+ require 'todo/helpers/object/presence'
4
+
5
+ module Todo
6
+ class View < Struct.new(:items, :cols)
7
+ COLS = [:status, :text, :tags, :id]
8
+
9
+ include Helpers::Hash::Format, Helpers::Object::Presence
10
+
11
+ def render
12
+ items.map { |item| format(item) }
13
+ end
14
+
15
+ private
16
+
17
+ def format(item)
18
+ cols.map { |col| format_col(col, item.send(col)) }.compact.join(' ')
19
+ end
20
+
21
+ def format_col(col, value)
22
+ send(:"format_#{col}", value) if present?(value)
23
+ end
24
+
25
+ def format_status(status)
26
+ STATUSES[status]
27
+ end
28
+
29
+ def format_id(id)
30
+ "[#{id}]"
31
+ end
32
+
33
+ def format_text(text)
34
+ text
35
+ end
36
+
37
+ def format_done_date(date)
38
+ date
39
+ end
40
+
41
+ def format_tags(tags)
42
+ to_pairs(tags, ':') if tags.any?
43
+ end
44
+
45
+ def cols
46
+ super || COLS
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,9 @@
1
+ require 'time'
2
+ require 'support/io'
3
+
4
+ NOW = Time.parse('2015-12-01 00:02:00 +0200')
5
+
6
+ RSpec.configure do |c|
7
+ c.mock_with :mocha
8
+ c.before { Time.stubs(:now).returns(NOW) }
9
+ end
@@ -0,0 +1,9 @@
1
+ require 'stringio'
2
+
3
+ module Support
4
+ class Io < StringIO
5
+ def readlines
6
+ string.split("\n")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ require 'todo/cli/archive'
2
+ require 'fileutils'
3
+
4
+ describe Todo::Cli::Archive do
5
+ let(:lines) { ['- foo [1]', 'x bar done:2015-11-01 [2]', 'x baz done:2015-12-01 [3]'] }
6
+ let(:io) { Support::Io.new(lines.join("\n")) }
7
+ let(:out) { Support::Io.new }
8
+ let(:opts) { { archive: '/tmp/todo.archive.txt', in: io, out: out } }
9
+
10
+ subject { described_class.new(nil, opts) }
11
+ before { subject.run }
12
+ after { FileUtils.rm_f(opts[:archive]) }
13
+
14
+ it { expect(out.readlines).to eq ['- foo [1]', 'x baz done:2015-12-01 [3]'] }
15
+ it { expect(File.read(opts[:archive]).strip).to eq 'x bar done:2015-11-01 [2]' }
16
+ end