aoc_cli 0.2.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (151) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +88 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +8 -0
  6. data/Gemfile.lock +187 -0
  7. data/README.md +88 -282
  8. data/Rakefile +9 -2
  9. data/Steepfile +13 -0
  10. data/aoc_cli.gemspec +36 -26
  11. data/db/migrate/1_create_events.rb +14 -0
  12. data/db/migrate/2_create_puzzles.rb +21 -0
  13. data/db/migrate/3_create_stats.rb +19 -0
  14. data/db/migrate/4_create_attempts.rb +19 -0
  15. data/db/migrate/5_create_locations.rb +17 -0
  16. data/db/migrate/6_create_puzzle_dir_sync_logs.rb +18 -0
  17. data/exe/aoc +5 -0
  18. data/lib/aoc_cli/components/attempts_table.erb +5 -0
  19. data/lib/aoc_cli/components/attempts_table.rb +58 -0
  20. data/lib/aoc_cli/components/docs_component.erb +19 -0
  21. data/lib/aoc_cli/components/docs_component.rb +41 -0
  22. data/lib/aoc_cli/components/errors_component.erb +8 -0
  23. data/lib/aoc_cli/components/errors_component.rb +36 -0
  24. data/lib/aoc_cli/components/progress_table.erb +1 -0
  25. data/lib/aoc_cli/components/progress_table.rb +43 -0
  26. data/lib/aoc_cli/components/puzzle_sync_component.erb +2 -0
  27. data/lib/aoc_cli/components/puzzle_sync_component.rb +26 -0
  28. data/lib/aoc_cli/configurators/session_configurator.rb +7 -0
  29. data/lib/aoc_cli/controllers/application_controller.rb +27 -0
  30. data/lib/aoc_cli/controllers/concerns/error_concern.rb +17 -0
  31. data/lib/aoc_cli/controllers/concerns/location_concern.rb +79 -0
  32. data/lib/aoc_cli/controllers/default_controller.rb +9 -0
  33. data/lib/aoc_cli/controllers/event_controller.rb +35 -0
  34. data/lib/aoc_cli/controllers/help/event_controller.rb +11 -0
  35. data/lib/aoc_cli/controllers/help/puzzle_controller.rb +13 -0
  36. data/lib/aoc_cli/controllers/puzzle_controller.rb +39 -0
  37. data/lib/aoc_cli/core/attempt_parser.rb +69 -0
  38. data/lib/aoc_cli/core/processor.rb +32 -0
  39. data/lib/aoc_cli/core/repository.rb +74 -0
  40. data/lib/aoc_cli/core/request.rb +37 -0
  41. data/lib/aoc_cli/core/resource.rb +39 -0
  42. data/lib/aoc_cli/core/stats_parser.rb +43 -0
  43. data/lib/aoc_cli/helpers/table_generator.rb +64 -0
  44. data/lib/aoc_cli/helpers/view_helper.rb +35 -0
  45. data/lib/aoc_cli/models/attempt.rb +67 -0
  46. data/lib/aoc_cli/models/event.rb +9 -0
  47. data/lib/aoc_cli/models/location.rb +24 -0
  48. data/lib/aoc_cli/models/puzzle.rb +28 -0
  49. data/lib/aoc_cli/models/puzzle_dir_sync_log.rb +14 -0
  50. data/lib/aoc_cli/models/stats.rb +43 -0
  51. data/lib/aoc_cli/presenters/attempt_presenter.rb +29 -0
  52. data/lib/aoc_cli/presenters/puzzle_presenter.rb +29 -0
  53. data/lib/aoc_cli/presenters/stats_presenter.rb +32 -0
  54. data/lib/aoc_cli/processors/event_initialiser.rb +64 -0
  55. data/lib/aoc_cli/processors/puzzle_dir_synchroniser.rb +80 -0
  56. data/lib/aoc_cli/processors/puzzle_initialiser.rb +91 -0
  57. data/lib/aoc_cli/processors/puzzle_refresher.rb +27 -0
  58. data/lib/aoc_cli/processors/resource_attacher.rb +22 -0
  59. data/lib/aoc_cli/processors/solution_poster.rb +72 -0
  60. data/lib/aoc_cli/processors/stats_initialiser.rb +36 -0
  61. data/lib/aoc_cli/processors/stats_refresher.rb +23 -0
  62. data/lib/aoc_cli/validators/collection_type_validator.rb +57 -0
  63. data/lib/aoc_cli/validators/event_year_validator.rb +42 -0
  64. data/lib/aoc_cli/validators/included_validator.rb +21 -0
  65. data/lib/aoc_cli/validators/integer_validator.rb +32 -0
  66. data/lib/aoc_cli/validators/path_validator.rb +39 -0
  67. data/lib/aoc_cli/validators/type_validator.rb +54 -0
  68. data/lib/aoc_cli/version.rb +1 -1
  69. data/lib/aoc_cli/views/event/attach.erb +3 -0
  70. data/lib/aoc_cli/views/event/init.erb +3 -0
  71. data/lib/aoc_cli/views/help/event/attach.erb +32 -0
  72. data/lib/aoc_cli/views/help/event/init.erb +38 -0
  73. data/lib/aoc_cli/views/help/event/progress.erb +12 -0
  74. data/lib/aoc_cli/views/help/puzzle/attempts.erb +12 -0
  75. data/lib/aoc_cli/views/help/puzzle/init.erb +31 -0
  76. data/lib/aoc_cli/views/help/puzzle/solve.erb +33 -0
  77. data/lib/aoc_cli/views/help/puzzle/sync.erb +32 -0
  78. data/lib/aoc_cli/views/puzzle/init.erb +3 -0
  79. data/lib/aoc_cli/views/puzzle/solve.erb +10 -0
  80. data/lib/aoc_cli.rb +32 -16
  81. data/rbs_collection.lock.yaml +168 -0
  82. data/rbs_collection.yaml +28 -0
  83. data/sig/aoc_cli/components/attempts_table.rbs +29 -0
  84. data/sig/aoc_cli/components/docs_component.rbs +15 -0
  85. data/sig/aoc_cli/components/errors_component.rbs +19 -0
  86. data/sig/aoc_cli/components/progress_table.rbs +28 -0
  87. data/sig/aoc_cli/components/puzzle_sync_component.rbs +19 -0
  88. data/sig/aoc_cli/configurators/session_configurator.rbs +7 -0
  89. data/sig/aoc_cli/controllers/application_controller.rbs +12 -0
  90. data/sig/aoc_cli/controllers/concerns/error_concern.rbs +9 -0
  91. data/sig/aoc_cli/controllers/concerns/location_concern.rbs +35 -0
  92. data/sig/aoc_cli/controllers/default_controller.rbs +7 -0
  93. data/sig/aoc_cli/controllers/event_controller.rbs +18 -0
  94. data/sig/aoc_cli/controllers/help/event_controller.rbs +11 -0
  95. data/sig/aoc_cli/controllers/help/puzzle_controller.rbs +13 -0
  96. data/sig/aoc_cli/controllers/puzzle_controller.rbs +15 -0
  97. data/sig/aoc_cli/core/attempt_parser.rbs +41 -0
  98. data/sig/aoc_cli/core/processor.rbs +25 -0
  99. data/sig/aoc_cli/core/repository.rbs +25 -0
  100. data/sig/aoc_cli/core/request.rbs +29 -0
  101. data/sig/aoc_cli/core/resource.rbs +25 -0
  102. data/sig/aoc_cli/core/stats_parser.rbs +23 -0
  103. data/sig/aoc_cli/helpers/table_generator.rbs +36 -0
  104. data/sig/aoc_cli/helpers/view_helper.rbs +15 -0
  105. data/sig/aoc_cli/models/attempt.rbs +35 -0
  106. data/sig/aoc_cli/models/event.rbs +17 -0
  107. data/sig/aoc_cli/models/location.rbs +19 -0
  108. data/sig/aoc_cli/models/puzzle.rbs +28 -0
  109. data/sig/aoc_cli/models/puzzle_dir_sync_log.rbs +11 -0
  110. data/sig/aoc_cli/models/stats.rbs +53 -0
  111. data/sig/aoc_cli/presenters/attempt_presenter.rbs +13 -0
  112. data/sig/aoc_cli/presenters/puzzle_presenter.rbs +19 -0
  113. data/sig/aoc_cli/presenters/stats_presenter.rbs +19 -0
  114. data/sig/aoc_cli/processors/event_initialiser.rbs +26 -0
  115. data/sig/aoc_cli/processors/puzzle_dir_synchroniser.rbs +40 -0
  116. data/sig/aoc_cli/processors/puzzle_initialiser.rbs +37 -0
  117. data/sig/aoc_cli/processors/puzzle_refresher.rbs +21 -0
  118. data/sig/aoc_cli/processors/resource_attacher.rbs +16 -0
  119. data/sig/aoc_cli/processors/solution_poster.rbs +34 -0
  120. data/sig/aoc_cli/processors/stats_initialiser.rbs +15 -0
  121. data/sig/aoc_cli/processors/stats_refresher.rbs +19 -0
  122. data/sig/aoc_cli/validators/collection_type_validator.rbs +24 -0
  123. data/sig/aoc_cli/validators/event_year_validator.rbs +19 -0
  124. data/sig/aoc_cli/validators/included_validator.rbs +11 -0
  125. data/sig/aoc_cli/validators/integer_validator.rbs +15 -0
  126. data/sig/aoc_cli/validators/path_validator.rbs +17 -0
  127. data/sig/aoc_cli/validators/type_validator.rbs +22 -0
  128. data/sig/aoc_cli.rbs +6 -0
  129. data/sig/http.rbs +3 -0
  130. data/sig/kangaru.rbs +5 -0
  131. data/sig/nokogiri.rbs +3 -0
  132. data/sig/reverse_markdown.rbs +3 -0
  133. metadata +142 -34
  134. data/.gitignore +0 -5
  135. data/bin/aoc +0 -4
  136. data/bin/console +0 -15
  137. data/bin/setup +0 -7
  138. data/lib/aoc_cli/commands.rb +0 -232
  139. data/lib/aoc_cli/database.rb +0 -224
  140. data/lib/aoc_cli/day.rb +0 -124
  141. data/lib/aoc_cli/db/reddit.db +0 -0
  142. data/lib/aoc_cli/errors.rb +0 -275
  143. data/lib/aoc_cli/files.rb +0 -163
  144. data/lib/aoc_cli/help.rb +0 -77
  145. data/lib/aoc_cli/interface.rb +0 -81
  146. data/lib/aoc_cli/paths.rb +0 -101
  147. data/lib/aoc_cli/solve.rb +0 -104
  148. data/lib/aoc_cli/tables.rb +0 -138
  149. data/lib/aoc_cli/tools.rb +0 -120
  150. data/lib/aoc_cli/year.rb +0 -116
  151. data/sample/aoc.rc +0 -21
@@ -0,0 +1,74 @@
1
+ module AocCli
2
+ module Core
3
+ class Repository
4
+ HOST = "https://adventofcode.com".freeze
5
+
6
+ RESOURCES = {
7
+ stats: {
8
+ url: "%{host}/%{year}",
9
+ scope: "html/body/main/pre",
10
+ method: :get
11
+ },
12
+
13
+ puzzle: {
14
+ url: "%{host}/%{year}/day/%{day}",
15
+ scope: "html/body/main/article",
16
+ method: :get
17
+ },
18
+
19
+ input: {
20
+ url: "%{host}/%{year}/day/%{day}/input",
21
+ method: :get
22
+ },
23
+
24
+ solution: {
25
+ url: "%{host}/%{year}/day/%{day}/answer",
26
+ scope: "html/body/main/article",
27
+ method: :post,
28
+ params: %i[level answer]
29
+ }
30
+ }.freeze
31
+
32
+ class << self
33
+ def get_stats(year:)
34
+ html = build_resource(:stats, year:).fetch
35
+
36
+ StatsParser.new(html).to_h
37
+ end
38
+
39
+ def get_puzzle(year:, day:)
40
+ build_resource(:puzzle, year:, day:).fetch_markdown
41
+ end
42
+
43
+ def get_input(year:, day:)
44
+ build_resource(:input, year:, day:).fetch
45
+ end
46
+
47
+ def post_solution(year:, day:, level:, answer:)
48
+ response = build_resource(
49
+ :solution, year:, day:, level:, answer:
50
+ ).fetch_markdown
51
+
52
+ AttemptParser.new(response).to_h
53
+ end
54
+
55
+ private
56
+
57
+ def build_resource(type, **params)
58
+ attributes = RESOURCES[type]
59
+
60
+ Resource.new(
61
+ url: format_url(attributes[:url], **params),
62
+ scope: attributes[:scope],
63
+ method: attributes[:method],
64
+ params: params.slice(*attributes[:params])
65
+ )
66
+ end
67
+
68
+ def format_url(template, **params)
69
+ format(template, host: HOST, **params)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,37 @@
1
+ module AocCli
2
+ module Core
3
+ class Request
4
+ extend Forwardable
5
+
6
+ attr_reader :client
7
+
8
+ def_delegators :client, :get, :post
9
+
10
+ def initialize(token:)
11
+ @client = setup_client!(token)
12
+ end
13
+
14
+ def self.build
15
+ token = AocCli.config.session.token
16
+
17
+ raise "session token not set" if token.nil?
18
+
19
+ new(token:)
20
+ end
21
+
22
+ class << self
23
+ extend Forwardable
24
+
25
+ def_delegators :build, :get, :post
26
+ end
27
+
28
+ private
29
+
30
+ def setup_client!(token)
31
+ cookie = "session=#{token}"
32
+
33
+ HTTP.headers(Cookie: cookie)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,39 @@
1
+ module AocCli
2
+ module Core
3
+ # Scope filters the response with the given xpath which ensures that only
4
+ # desired content is extracted from the (usually) HTML response. The whole
5
+ # response is considered the resource payload if no scope is specified.
6
+ class Resource
7
+ attr_reader :url, :scope, :method, :params
8
+
9
+ def initialize(url:, scope: nil, method: :get, params: {})
10
+ @url = url
11
+ @scope = scope
12
+ @method = method
13
+ @params = params
14
+ end
15
+
16
+ def fetch
17
+ return response if scope.nil?
18
+
19
+ Nokogiri.HTML5(response).xpath(scope).to_html
20
+ end
21
+
22
+ # Bypess ignores unknown tags and tries to convert their nested content.
23
+ # The parser adds an extra newline to the end which is removed with chomp.
24
+ def fetch_markdown
25
+ ReverseMarkdown.convert(fetch, unknown_tags: :bypass).chomp
26
+ end
27
+
28
+ private
29
+
30
+ def response
31
+ case method
32
+ when :get then Request.get(url)
33
+ when :post then Request.post(url, form: params)
34
+ else raise "invalid HTTP method"
35
+ end.to_s
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,43 @@
1
+ module AocCli
2
+ module Core
3
+ # Calculates a User's year progress by parsing the CSS classes that are
4
+ # applied to each calendar link in the calendar view.
5
+ class StatsParser
6
+ DAY_CLASS_PREFIX = "calendar-day".freeze
7
+ ONE_STAR_CLASS = "calendar-complete".freeze
8
+ TWO_STARS_CLASS = "calendar-verycomplete".freeze
9
+
10
+ attr_reader :calendar_html
11
+
12
+ def initialize(calendar_html)
13
+ @calendar_html = calendar_html
14
+ end
15
+
16
+ def to_h
17
+ calendar_link_classes.to_h do |classes|
18
+ day = classes[0].delete_prefix(DAY_CLASS_PREFIX)
19
+ n_stars = count_stars(classes)
20
+
21
+ [:"day_#{day}", n_stars]
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def calendar_link_classes
28
+ calendar_links.map(&:classes)
29
+ end
30
+
31
+ def calendar_links
32
+ Nokogiri.HTML5(calendar_html).xpath("//a")
33
+ end
34
+
35
+ def count_stars(classes)
36
+ return 2 if classes.include?(TWO_STARS_CLASS)
37
+ return 1 if classes.include?(ONE_STAR_CLASS)
38
+
39
+ 0
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,64 @@
1
+ module AocCli
2
+ module Helpers
3
+ class TableGenerator
4
+ include Kangaru::Validatable
5
+
6
+ attr_reader :rows, :gap, :indent
7
+
8
+ validates :rows, collection_type: { all: Array }
9
+
10
+ def initialize(rows:, gap: 2, indent: 0)
11
+ @rows = rows
12
+ @gap = gap
13
+ @indent = indent
14
+ end
15
+
16
+ # TODO: Use validates via method for checking row length once supported.
17
+ def validate
18
+ super
19
+ validate_rows_are_same_length! if errors.empty?
20
+ end
21
+
22
+ # TODO: Remove once validate! merged upstream.
23
+ def validate!
24
+ validate
25
+
26
+ raise errors.map(&:full_message).join("\n") unless errors.empty?
27
+ end
28
+
29
+ def generate!
30
+ validate!
31
+
32
+ rows.map do |row|
33
+ [space(indent), format_row!(row).strip, "\n"].join
34
+ end.join
35
+ end
36
+
37
+ private
38
+
39
+ def column_widths
40
+ @column_widths ||= rows.transpose.map do |column|
41
+ column.map(&:length).max + gap
42
+ end
43
+ end
44
+
45
+ def space(count)
46
+ " " * count
47
+ end
48
+
49
+ def format_row!(row)
50
+ row.zip(column_widths).map do |string, column_width|
51
+ string + space((column_width || raise) - string.length)
52
+ end.join
53
+ end
54
+
55
+ def validate_rows_are_same_length!
56
+ return if rows.all? { |row| row.length == rows[0].length }
57
+
58
+ errors << Kangaru::Validation::Error.new(
59
+ attribute: :rows, message: "must all be the same length"
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,35 @@
1
+ module AocCli
2
+ module Helpers
3
+ module ViewHelper
4
+ def main_header
5
+ heading("aoc-cli::<#{VERSION}>")
6
+ end
7
+
8
+ def heading(text)
9
+ text.to_s.bold.cyan
10
+ end
11
+
12
+ def success_tag
13
+ "Success".green.bold
14
+ end
15
+
16
+ def table_for(*rows, gap: 2, indent: 0)
17
+ rows.map! { |row| row.map(&:to_s) }
18
+
19
+ TableGenerator.new(rows:, gap:, indent:).generate!
20
+ end
21
+
22
+ def wrap_text(text, width: 80, indent: 0)
23
+ raise "indent must be less than width" unless indent < width
24
+
25
+ text.gsub(
26
+ # Match the longest string (up to the indented width) thats followed
27
+ # by a non-word char or any combination of newlines.
28
+ /(.{1,#{width - indent}})(?:[^\S\n]+\n?|\n*\Z|\n)|\n/,
29
+ # Surround string fragment with indent and newline.
30
+ "#{' ' * indent}\\1\n"
31
+ ).chomp
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,67 @@
1
+ module AocCli
2
+ class Attempt < Kangaru::Model
3
+ many_to_one :puzzle
4
+
5
+ enum :status, incorrect: 0, correct: 1, rate_limited: 2, wrong_level: 3
6
+
7
+ enum :hint, too_low: 0, too_high: 1
8
+
9
+ validates :puzzle, required: true
10
+
11
+ validates :level, integer: { between: 1..2 }
12
+
13
+ validates :answer, required: true
14
+
15
+ validates :status, included: {
16
+ in: %i[incorrect correct rate_limited wrong_level]
17
+ }
18
+
19
+ def presenter
20
+ @presenter ||= Presenters::AttemptPresenter.new(self)
21
+ end
22
+
23
+ # TODO: move to a conditional validation when supported by Kangaru
24
+ def validate
25
+ super
26
+ validate_hint_not_set! unless incorrect?
27
+ validate_hint! if incorrect?
28
+ validate_wait_time_not_set! if correct? || wrong_level?
29
+ validate_wait_time_integer! if incorrect? || rate_limited?
30
+ end
31
+
32
+ private
33
+
34
+ def validate_hint_not_set!
35
+ return if hint.nil?
36
+
37
+ errors << Kangaru::Validation::Error.new(
38
+ attribute: :hint, message: "is not expected"
39
+ )
40
+ end
41
+
42
+ def validate_hint!
43
+ return if hint.nil?
44
+ return if %i[too_low too_high].include?(hint)
45
+
46
+ errors << Kangaru::Validation::Error.new(
47
+ attribute: :hint, message: "is not a valid option"
48
+ )
49
+ end
50
+
51
+ def validate_wait_time_not_set!
52
+ return if wait_time.nil?
53
+
54
+ errors << Kangaru::Validation::Error.new(
55
+ attribute: :wait_time, message: "is not expected"
56
+ )
57
+ end
58
+
59
+ def validate_wait_time_integer!
60
+ return if wait_time.is_a?(Integer)
61
+
62
+ errors << Kangaru::Validation::Error.new(
63
+ attribute: :wait_time, message: "is not an integer"
64
+ )
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,9 @@
1
+ module AocCli
2
+ class Event < Kangaru::Model
3
+ one_to_many :puzzles
4
+ one_to_one :stats
5
+ one_to_one :location, as: :resource
6
+
7
+ validates :year, event_year: true
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ module AocCli
2
+ class Location < Kangaru::Model
3
+ many_to_one :resource, polymorphic: true
4
+
5
+ validates :resource, required: true
6
+ validates :path, required: true
7
+
8
+ def exists?
9
+ File.exist?(path)
10
+ end
11
+
12
+ def event_dir?
13
+ resource.is_a?(Event)
14
+ end
15
+
16
+ def puzzle_dir?
17
+ resource.is_a?(Puzzle)
18
+ end
19
+
20
+ def to_pathname
21
+ Pathname.new(path)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ module AocCli
2
+ class Puzzle < Kangaru::Model
3
+ extend Forwardable
4
+
5
+ many_to_one :event
6
+ one_to_one :location, as: :resource
7
+ one_to_many :attempts
8
+
9
+ validates :event, required: true
10
+ validates :day, integer: { between: 1..25 }
11
+ validates :content, required: true
12
+ validates :input, required: true
13
+
14
+ def_delegators :event, :year
15
+
16
+ def presenter
17
+ @presenter ||= Presenters::PuzzlePresenter.new(self)
18
+ end
19
+
20
+ def mark_complete!(level)
21
+ case level
22
+ when 1 then update(part_one_completed_at: Time.now)
23
+ when 2 then update(part_two_completed_at: Time.now)
24
+ else raise "invalid level"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ module AocCli
2
+ class PuzzleDirSyncLog < Kangaru::Model
3
+ many_to_one :location
4
+ many_to_one :puzzle
5
+
6
+ validates :location, required: true
7
+ validates :puzzle, required: true
8
+
9
+ STATUS_ENUM = { new: 0, unmodified: 1, modified: 2 }.freeze
10
+
11
+ enum :puzzle_status, STATUS_ENUM, prefix: true
12
+ enum :input_status, STATUS_ENUM, prefix: true
13
+ end
14
+ end
@@ -0,0 +1,43 @@
1
+ module AocCli
2
+ class Stats < Kangaru::Model
3
+ extend Forwardable
4
+
5
+ many_to_one :event
6
+
7
+ validates :event, required: true
8
+
9
+ def_delegators :event, :year
10
+
11
+ 1.upto(25) do |i|
12
+ validates :"day_#{i}", integer: { between: 0..2 }
13
+ end
14
+
15
+ def presenter
16
+ @presenter ||= Presenters::StatsPresenter.new(self)
17
+ end
18
+
19
+ def total
20
+ 1.upto(25).map { |day| progress(day) }.sum
21
+ end
22
+
23
+ def progress(day)
24
+ self[:"day_#{day}"] || raise("invalid day")
25
+ end
26
+
27
+ def current_level(day)
28
+ return if complete?(day)
29
+
30
+ progress(day) + 1
31
+ end
32
+
33
+ def complete?(day)
34
+ progress(day) == 2
35
+ end
36
+
37
+ def advance_progress!(day)
38
+ raise "already complete" if complete?(day)
39
+
40
+ update("day_#{day}": current_level(day))
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,29 @@
1
+ module AocCli
2
+ module Presenters
3
+ class AttemptPresenter
4
+ attr_reader :attempt
5
+
6
+ def initialize(attempt)
7
+ @attempt = attempt
8
+ end
9
+
10
+ def status
11
+ case attempt.status
12
+ when :wrong_level then "Wrong level"
13
+ when :rate_limited then "Rate limited"
14
+ when :incorrect then "Incorrect"
15
+ when :correct then "Correct"
16
+ else raise
17
+ end
18
+ end
19
+
20
+ def hint
21
+ case attempt.hint
22
+ when :too_low then "Too low"
23
+ when :too_high then "Too high"
24
+ else "-"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ module AocCli
2
+ module Presenters
3
+ class PuzzlePresenter
4
+ attr_reader :puzzle
5
+
6
+ def initialize(puzzle)
7
+ @puzzle = puzzle
8
+ end
9
+
10
+ def date
11
+ "#{puzzle.event.year}-12-#{formatted_day}"
12
+ end
13
+
14
+ def puzzle_filename
15
+ "day_#{formatted_day}.md"
16
+ end
17
+
18
+ def input_filename
19
+ "input"
20
+ end
21
+
22
+ private
23
+
24
+ def formatted_day
25
+ puzzle.day.to_s.rjust(2, "0")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ module AocCli
2
+ module Presenters
3
+ class StatsPresenter
4
+ attr_reader :stats
5
+
6
+ def initialize(stats)
7
+ @stats = stats
8
+ end
9
+
10
+ def total_progress
11
+ total = 1.upto(25).map { |day| stats.progress(day) }.sum
12
+
13
+ "#{total}/50"
14
+ end
15
+
16
+ def progress_icons(day)
17
+ case stats.progress(day)
18
+ when 0 then Icons::INCOMPLETE
19
+ when 1 then Icons::HALF_COMPLETE
20
+ when 2 then Icons::COMPLETE
21
+ else raise
22
+ end
23
+ end
24
+
25
+ module Icons
26
+ INCOMPLETE = "○ ○".freeze
27
+ HALF_COMPLETE = "● ○".freeze
28
+ COMPLETE = "● ●".freeze
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,64 @@
1
+ module AocCli
2
+ module Processors
3
+ class EventInitialiser < Core::Processor
4
+ validates :year, event_year: true
5
+
6
+ validates :dir, path: { exists: true }
7
+
8
+ attr_accessor :year, :dir
9
+
10
+ # TODO: use conditional validation once supported by Kangaru
11
+ def validate
12
+ super
13
+ validate_event_not_already_initialised! if errors.empty?
14
+ validate_event_dir_does_not_exist! if errors.empty?
15
+ end
16
+
17
+ def run
18
+ create_event!.tap do |event|
19
+ initialise_stats!(event)
20
+ make_event_directory!
21
+ attach_event!(event)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def event_dir
28
+ @event_dir ||= Pathname(dir).join(year.to_s)
29
+ end
30
+
31
+ def create_event!
32
+ Event.create(year:)
33
+ end
34
+
35
+ def initialise_stats!(event)
36
+ StatsInitialiser.run!(event:)
37
+ end
38
+
39
+ def make_event_directory!
40
+ event_dir.mkdir
41
+ end
42
+
43
+ def attach_event!(event)
44
+ ResourceAttacher.run!(resource: event, path: event_dir.to_s)
45
+ end
46
+
47
+ def validate_event_dir_does_not_exist!
48
+ return unless event_dir.exist?
49
+
50
+ errors << Kangaru::Validation::Error.new(
51
+ attribute: :event_dir, message: "already exists"
52
+ )
53
+ end
54
+
55
+ def validate_event_not_already_initialised!
56
+ return if Event.where(year:).empty?
57
+
58
+ errors << Kangaru::Validation::Error.new(
59
+ attribute: :event, message: "already initialised"
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end