ankit 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. data/bin/ankit +6 -0
  2. data/lib/ankit/add_command.rb +28 -0
  3. data/lib/ankit/card.rb +66 -0
  4. data/lib/ankit/card_happening_command.rb +31 -0
  5. data/lib/ankit/challenge_command.rb +444 -0
  6. data/lib/ankit/coming_command.rb +90 -0
  7. data/lib/ankit/command.rb +40 -0
  8. data/lib/ankit/event.rb +62 -0
  9. data/lib/ankit/event_traversing_command.rb +37 -0
  10. data/lib/ankit/fail_command.rb +14 -0
  11. data/lib/ankit/find_command.rb +38 -0
  12. data/lib/ankit/hello_command.rb +22 -0
  13. data/lib/ankit/list_command.rb +26 -0
  14. data/lib/ankit/name_command.rb +16 -0
  15. data/lib/ankit/pass_command.rb +9 -0
  16. data/lib/ankit/round_command.rb +31 -0
  17. data/lib/ankit/runtime.rb +151 -0
  18. data/lib/ankit/score_command.rb +24 -0
  19. data/lib/ankit/text_reading_command.rb +23 -0
  20. data/lib/ankit.rb +3 -0
  21. data/test/card_test.rb +92 -0
  22. data/test/command_test.rb +406 -0
  23. data/test/data/bye_card.card +2 -0
  24. data/test/data/hello_card.card +2 -0
  25. data/test/data/hello_config.rb +4 -0
  26. data/test/data/hello_repo/anemone.journal +2 -0
  27. data/test/data/hello_repo/baobab.journal +2 -0
  28. data/test/data/hello_repo/cards/foo/hello.card +2 -0
  29. data/test/data/hello_repo/cards/foo/this_is_not_a_card.txt +1 -0
  30. data/test/data/hello_repo/cards/foo/vanilla-please.card +2 -0
  31. data/test/data/hope.card +1 -0
  32. data/test/data/luck.card +1 -0
  33. data/test/data/number_repo/anemone.journal +0 -0
  34. data/test/data/number_repo/cards/eight.card +2 -0
  35. data/test/data/number_repo/cards/five.card +2 -0
  36. data/test/data/number_repo/cards/four.card +2 -0
  37. data/test/data/number_repo/cards/one.card +2 -0
  38. data/test/data/number_repo/cards/seven.card +2 -0
  39. data/test/data/number_repo/cards/six.card +2 -0
  40. data/test/data/number_repo/cards/three.card +2 -0
  41. data/test/data/number_repo/cards/two.card +2 -0
  42. data/test/data/vanilla_repo/anemone.journal +0 -0
  43. data/test/event_test.rb +29 -0
  44. data/test/helpers.rb +54 -0
  45. data/test/progress_test.rb +99 -0
  46. data/test/runtime_test.rb +58 -0
  47. metadata +138 -0
@@ -0,0 +1,62 @@
1
+
2
+ require 'date'
3
+ require 'json'
4
+
5
+ module Ankit
6
+ class Envelope < Struct.new(:at, :round)
7
+ def to_json(*a)
8
+ { at: self.at.rfc3339, round: self.round }.to_json(*a)
9
+ end
10
+
11
+ def self.from_hash(hash)
12
+ Envelope.new(DateTime.rfc3339(hash["at"]), hash["round"])
13
+ end
14
+
15
+ def self.parse(text) from_hash(JSON.parse(text)); end
16
+ def self.fresh(round=0); self.new(DateTime.new, round); end
17
+ end
18
+
19
+ class Event
20
+ attr_reader :envelope, :values
21
+
22
+ def verb() @values["verb"]; end
23
+ def verb=(val) @values["verb"] = val; end
24
+ def name() @values["name"]; end
25
+ def type() @values["type"]; end
26
+ def maturity() @values["maturity"] || 0; end
27
+ def card?() type == "card"; end
28
+ def round() @envelope.round or 0; end
29
+ def next_round() round + 2**maturity; end
30
+
31
+ def initialize(env, values)
32
+ @envelope, @values = env, values
33
+ end
34
+
35
+ def ==(other)
36
+ @values == other.values && @envelope == other.envelope
37
+ end
38
+
39
+ def to_json(*a) { envelope: @envelope, values: @values }.to_json(*a); end
40
+
41
+ def to_passed(env)
42
+ Event.new(env, @values.merge({ "verb" => "passed", "maturity" => maturity + 1 }))
43
+ end
44
+
45
+ def to_failed(env)
46
+ Event.new(env, @values.merge({ "verb" => "failed", "maturity" => 0 }))
47
+ end
48
+
49
+ def self.for_card(name, verb, env)
50
+ self.new(env, { "type" => "card", "verb" => verb, "name" => name, "maturity" => 0 })
51
+ end
52
+
53
+ def self.from_hash(hash) Event.new(Envelope.from_hash(hash["envelope"]), hash["values"]); end
54
+ def self.parse(text) from_hash(JSON.parse(text)); end
55
+ end
56
+
57
+ module EventFormatting
58
+ def format_as_score(event)
59
+ "name:#{event.name}, verb:#{event.verb}, round:#{event.round}, maturity:#{event.maturity}"
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,37 @@
1
+
2
+ require 'ankit/command'
3
+ require 'ankit/card'
4
+
5
+ module Ankit
6
+ class EventTraversingCommand < Command
7
+ def each_event(name=nil, &block)
8
+ runtime.config.journals.each do |j|
9
+ open(j) do |f|
10
+ f.each_line do |line|
11
+ event = Event.parse(line)
12
+ block.call(event) if nil == name or event.name == name
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ # For Command Mixin
20
+ module EventTraversing
21
+ class << self
22
+ include CardNaming
23
+ end
24
+
25
+ def self.find_latest_event_for(runtime, path)
26
+ #EventTraversingCommand.new(runtime).to_enum(:each_event, to_card_name(path)).sort_by { |x| x.round }[-1]
27
+ find_latest_event_named(runtime, to_card_name(path))
28
+ end
29
+
30
+ def self.find_latest_event_named(runtime, name)
31
+ EventTraversingCommand.new(runtime).to_enum(:each_event, name).sort_by { |x| x.round }[-1]
32
+ end
33
+
34
+ def latest_event_for(path) EventTraversing.find_latest_event_for(self.runtime, path); end
35
+ end
36
+
37
+ end
@@ -0,0 +1,14 @@
1
+
2
+ require 'ankit/card_happening_command'
3
+
4
+ module Ankit
5
+ class FailCommand < CardHappeningCommand
6
+ available
7
+ EVENT_HAPPENING = :to_failed
8
+ end
9
+
10
+ module Failing
11
+ include CardHappening
12
+ def make_failed(card_name); make_happen(FailCommand::EVENT_HAPPENING, card_name); end
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+
2
+ require 'ankit/card'
3
+ require 'ankit/command'
4
+ require 'ankit/find_command'
5
+
6
+ module Ankit
7
+ class FindCommand < Command
8
+ include CardNaming
9
+ available
10
+
11
+ def execute()
12
+ each_path { |f| runtime.stdout.print("#{f}\n") }
13
+ end
14
+
15
+ def each_path(&block)
16
+ names.each do |n|
17
+ found = path_for(n)
18
+ block.call(found) if found
19
+ end
20
+ end
21
+
22
+ def path_for(name)
23
+ found_in = runtime.config.card_search_paths.find do |p|
24
+ File.file?(to_card_path(p, name))
25
+ end
26
+
27
+ found_in ? to_card_path(found_in, name) : nil
28
+ end
29
+
30
+ def names; args; end
31
+ end
32
+
33
+ module Finding
34
+ def find_paths(runtime, names)
35
+ FindCommand.new(runtime, names).to_enum(:each_path).to_a
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+
2
+ require 'ankit/command'
3
+
4
+ module Ankit
5
+ class HelloCommand < Command
6
+ available
7
+
8
+ def execute()
9
+ indent = " "
10
+ runtime.stdout.print("\n")
11
+ runtime.stdout.print(indent, "repo: #{runtime.config.repo}\n")
12
+ runtime.stdout.print(indent, "primary: #{runtime.config.primary_journal}\n")
13
+ runtime.stdout.print(indent, "location: #{runtime.config.location}\n")
14
+
15
+ runtime.config.card_search_paths.each do |p|
16
+ runtime.stdout.print(indent, "card_search_paths: #{p}\n")
17
+ end
18
+
19
+ runtime.stdout.print("\n")
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+
2
+ require 'ankit/card'
3
+ require 'ankit/command'
4
+
5
+ module Ankit
6
+ class ListCommand < Command
7
+ include CardNaming
8
+ available
9
+
10
+ def execute()
11
+ each_card { |f| runtime.stdout.print("#{f}\n") }
12
+ end
13
+
14
+ def each_card(&block)
15
+ runtime.config.card_search_paths.each do |p|
16
+ Dir.glob(card_wildcard_for(p)).each do |f|
17
+ block.call(f)
18
+ end
19
+ end
20
+ end
21
+
22
+ def each_card_name(&block)
23
+ each_card { |path| block.call(to_card_name(path)) }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+
2
+ require 'ankit/text_reading_command'
3
+
4
+ module Ankit
5
+ class NameCommand < TextReadingCommand
6
+ available
7
+ define_options { |s, o| superclass.option_spec.call(s, o) }
8
+
9
+ def execute()
10
+ validate_options
11
+ each_text do |text|
12
+ runtime.stdout.print("#{Card.parse(text).name}\n")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+
2
+ require 'ankit/card_happening_command'
3
+
4
+ module Ankit
5
+ class PassCommand < CardHappeningCommand
6
+ available
7
+ EVENT_HAPPENING = :to_passed
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+
2
+ require 'ankit/card'
3
+ #require 'ankit/event_traversing_command'
4
+ require 'ankit/coming_command'
5
+
6
+ module Ankit
7
+ class RoundCommand < Command
8
+ available
9
+
10
+ def execute()
11
+ runtime.stdout.print("#{last_round} #{next_round}\n")
12
+ end
13
+
14
+ def next_round
15
+ found = Coming.existing_events(runtime).first
16
+ found ? found.next_round : 0
17
+ end
18
+
19
+ def last_round
20
+ found = Coming.existing_events(runtime).max_by(&:round)
21
+ found ? found.round : 0
22
+ end
23
+ end
24
+
25
+ module RoundCounting
26
+ def last_round; @last_round ||= RoundCommand.new(self.runtime).last_round; end
27
+ def next_round; @next_round ||= RoundCommand.new(self.runtime).next_round; end
28
+ def latest_round; last_round + 1; end
29
+ def round_proceeded; @last_round = @next_round = nil; end
30
+ end
31
+ end
@@ -0,0 +1,151 @@
1
+
2
+ require 'optparse'
3
+ require 'fileutils'
4
+ require 'highline'
5
+ require 'ankit/command'
6
+ require 'ankit/add_command'
7
+ require 'ankit/challenge_command'
8
+ require 'ankit/coming_command'
9
+ require 'ankit/fail_command'
10
+ require 'ankit/find_command'
11
+ require 'ankit/hello_command'
12
+ require 'ankit/list_command'
13
+ require 'ankit/name_command'
14
+ require 'ankit/pass_command'
15
+ require 'ankit/round_command'
16
+ require 'ankit/score_command'
17
+
18
+ module Ankit
19
+
20
+ class Config
21
+ DEFAULT_PATH = File.expand_path("~/.ankit")
22
+
23
+ attr_writer :repo, :location, :card_paths
24
+
25
+ def repo; @repo ||= File.expand_path("~/.ankit.d"); end
26
+ def location; @location ||= `hostname`.strip; end
27
+ def card_paths; @card_paths ||= [File.join(repo, "cards")]; end
28
+
29
+ # Computed parameters
30
+ def primary_journal
31
+ File.join(repo, "#{location}.journal")
32
+ end
33
+
34
+ def journals
35
+ Dir.glob(File.join(repo, "*.journal")).sort
36
+ end
37
+
38
+ def card_search_paths
39
+ paths = self.card_paths.dup
40
+ self.card_paths.each do |path|
41
+ Dir.glob(File.join(path, "*")).each do |f|
42
+ paths.push(f) if File.directory?(f)
43
+ end
44
+ end
45
+
46
+ paths.sort
47
+ end
48
+
49
+
50
+ def self.open(path)
51
+ config = self.new
52
+ config.instance_eval(File.open(path){ |f| f.read })
53
+ config
54
+ end
55
+
56
+ def self.prepare_default
57
+ FileUtils.touch([DEFAULT_PATH]) # TODO: Give same example settings.
58
+ plain = Config.open(DEFAULT_PATH)
59
+ (plain.card_paths + [plain.repo]).each { |p| FileUtils.mkdir_p(p) }
60
+ FileUtils.touch([plain.primary_journal])
61
+ STDOUT.print("Prepared the default setting. You can edit #{DEFAULT_PATH}\n")
62
+ end
63
+ end
64
+
65
+ class Runtime
66
+ attr_writer :line, :stdin, :stdout, :stderr
67
+ attr_reader :config
68
+
69
+ def line; @line ||= HighLine.new; end
70
+ def stdin; @stdin ||= STDIN; end
71
+ def stdout; @stdout ||= STDOUT; end
72
+ def stderr; @stderr ||= STDERR; end
73
+
74
+ def self.split_subcommand(args)
75
+ names = Command.by_name.keys
76
+ i = args.find_index { |a| names.include?(a) }
77
+ unless i.nil?
78
+ { global: args[0 ... i], subcommand: args[i .. -1] }
79
+ else
80
+ { global: [], subcommand: [] }
81
+ end
82
+ end
83
+
84
+ def self.parse_options(args)
85
+ options = {}
86
+ OptionParser.new do |spec|
87
+ spec.on("-c", "--config FILE", "Specifies config file") { |file| options[:config] = file }
88
+ end.parse(args)
89
+
90
+ options[:noconf] = options[:config].nil?
91
+ options[:config] ||= Config::DEFAULT_PATH
92
+ options
93
+ end
94
+
95
+ def self.setup(args)
96
+ options = self.parse_options(args)
97
+ Config.prepare_default if options[:noconf] and not File.exist?(Config::DEFAULT_PATH)
98
+ r = self.new(Config.open(options[:config]))
99
+ end
100
+
101
+ def self.run(args)
102
+ splitted = self.split_subcommand(args)
103
+ r = self.setup(splitted[:global])
104
+ if splitted[:subcommand].empty?
105
+ # TODO: show help
106
+ else
107
+ r.dispatch(splitted[:subcommand])
108
+ end
109
+ end
110
+
111
+ def initialize(config)
112
+ @config = config
113
+ end
114
+
115
+ def make_command(args)
116
+ name = args.shift
117
+ Command.by_name[name].new(self, args)
118
+ end
119
+
120
+ def dispatch(args)
121
+ command = make_command(args)
122
+ command.execute()
123
+ command
124
+ end
125
+
126
+ # To encourage one-liner
127
+ def dispatch_then(args)
128
+ dispatch(args)
129
+ self
130
+ end
131
+
132
+ def supress_io
133
+ saved = [@stdin, @stdout, @stderr]
134
+ ["stdin=", "stdout=", "stderr="].each { |m| self.send(m, StringIO.new) }
135
+ saved
136
+ end
137
+
138
+ def unsupress_io(saved)
139
+ @stdin, @stdout, @stderr = saved
140
+ end
141
+
142
+ def with_supressing_io(&block)
143
+ saved = supress_io
144
+ begin
145
+ block.call
146
+ ensure
147
+ unsupress_io(saved)
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,24 @@
1
+
2
+ require 'ankit/card'
3
+ require 'ankit/event'
4
+ require 'ankit/event_traversing_command'
5
+
6
+ module Ankit
7
+ class ScoreCommand < EventTraversingCommand
8
+ include CardNaming, EventFormatting
9
+ available
10
+
11
+ define_options do |spec, options|
12
+ spec.on("-l", "--last") { options[:last] = true }
13
+ end
14
+
15
+ def execute()
16
+ args.each do |a|
17
+ list = to_enum(:each_event, to_card_name(a)).to_a
18
+ (options[:last] ? list.sort_by(&:round).reverse.take(1) : list).each do |e|
19
+ runtime.stdout.print("#{format_as_score(e)}\n")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+
2
+ require 'ankit/command'
3
+
4
+ module Ankit
5
+ class TextReadingCommand < Command
6
+ define_options do |spec, options|
7
+ spec.on("-i", "--stdin") { options[:stdin] = true }
8
+ end
9
+
10
+ def each_text(&block)
11
+ if options[:stdin]
12
+ block.call(runtime.stdin.read)
13
+ else
14
+ args.each { |name| open(name) { |f| block.call(f.read) } }
15
+ end
16
+ end
17
+
18
+ def validate_options
19
+ raise BadOptions, "--stdin cannot have any fileame" if options[:stdin] and not args.empty?
20
+ raise BadOptions, "need a fileame" if not options[:stdin] and args.empty?
21
+ end
22
+ end
23
+ end
data/lib/ankit.rb ADDED
@@ -0,0 +1,3 @@
1
+
2
+ require 'ankit/runtime'
3
+
data/test/card_test.rb ADDED
@@ -0,0 +1,92 @@
1
+
2
+ require 'ankit/card'
3
+ require 'test/unit'
4
+
5
+ class CardTest < Test::Unit::TestCase
6
+ include Ankit
7
+
8
+ def test_parse_empty
9
+ actual = Card.parse(
10
+ """
11
+
12
+
13
+ """)
14
+ assert_nil(actual)
15
+ end
16
+
17
+ def test_parse_empty_comments
18
+ actual = Card.parse(
19
+ """
20
+ #
21
+ #
22
+ """)
23
+ assert_nil(actual)
24
+ end
25
+
26
+ def test_parse_hello
27
+ actual = Card.parse(
28
+ """
29
+ O: Hello, how are you?
30
+ T: Konichiwa, Genki?
31
+ """)
32
+
33
+ assert_equal("Konichiwa, Genki?", actual.translation)
34
+ assert_equal("Hello, how are you?", actual.original)
35
+ end
36
+
37
+ def test_name_plain
38
+ actual = Card.parse(
39
+ """
40
+ O: Hello, how are you?
41
+ T: Konichiwa, Genki?
42
+ """)
43
+ assert_equal("hello-how-are-you", actual.name)
44
+ end
45
+
46
+ def test_name_with_a_bracket
47
+ actual = Card.parse(
48
+ """
49
+ O: [Hello], How are you?
50
+ T: Konichiwa, Genki?
51
+ """)
52
+ assert_equal("hello-how-are-you", actual.name)
53
+ end
54
+
55
+ def test_name_with_brackets
56
+ actual = Card.parse(
57
+ """
58
+ O: Hello, [How is] your project [going]?
59
+ T: Konichiwa, Genki?
60
+ """)
61
+ assert_equal("hello-how-is-your-project-going", actual.name)
62
+ end
63
+
64
+ def test_guess_id_with_conflict
65
+ # XXX: will tackle later
66
+ end
67
+
68
+ def test_plain_original
69
+ assert_equal(Card.new(o: "Hello").plain_original, "Hello")
70
+ assert_equal(Card.new(o: "Hello, [World].").plain_original, "Hello, World.")
71
+ end
72
+
73
+ def test_match_hello
74
+ target = Card.new(o: "Hello")
75
+ assert( target.match?("Hello"))
76
+ assert_equal(:wrong, target.match?("Bye"))
77
+ end
78
+
79
+ def test_match_bracket
80
+ target = Card.new(o: "Hello, [World].")
81
+ assert_equal(:match, target.match?("Hello, World."))
82
+ end
83
+
84
+ def test_match_typo
85
+ target = Card.new(o: "Hello, [World].")
86
+ assert_equal(:typo, target.match?("Hallo, World."))
87
+ assert_equal(:typo, target.match?("Halo, World."))
88
+ assert_equal(:typo, target.match?("Hello, World!"))
89
+ assert_equal(:typo, target.match?("World!"))
90
+ assert_equal(:wrong, target.match?(""))
91
+ end
92
+ end