ankit 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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