aoc_cli 0.2.2 → 1.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 (152) 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/CHANGELOG.md +4 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +187 -0
  8. data/README.md +88 -282
  9. data/Rakefile +9 -2
  10. data/Steepfile +13 -0
  11. data/aoc_cli.gemspec +36 -26
  12. data/db/migrate/1_create_events.rb +14 -0
  13. data/db/migrate/2_create_puzzles.rb +21 -0
  14. data/db/migrate/3_create_stats.rb +19 -0
  15. data/db/migrate/4_create_attempts.rb +19 -0
  16. data/db/migrate/5_create_locations.rb +17 -0
  17. data/db/migrate/6_create_puzzle_dir_sync_logs.rb +18 -0
  18. data/exe/aoc +5 -0
  19. data/lib/aoc_cli/components/attempts_table.erb +5 -0
  20. data/lib/aoc_cli/components/attempts_table.rb +58 -0
  21. data/lib/aoc_cli/components/docs_component.erb +19 -0
  22. data/lib/aoc_cli/components/docs_component.rb +41 -0
  23. data/lib/aoc_cli/components/errors_component.erb +8 -0
  24. data/lib/aoc_cli/components/errors_component.rb +36 -0
  25. data/lib/aoc_cli/components/progress_table.erb +1 -0
  26. data/lib/aoc_cli/components/progress_table.rb +43 -0
  27. data/lib/aoc_cli/components/puzzle_sync_component.erb +2 -0
  28. data/lib/aoc_cli/components/puzzle_sync_component.rb +26 -0
  29. data/lib/aoc_cli/configurators/session_configurator.rb +7 -0
  30. data/lib/aoc_cli/controllers/application_controller.rb +27 -0
  31. data/lib/aoc_cli/controllers/concerns/error_concern.rb +17 -0
  32. data/lib/aoc_cli/controllers/concerns/location_concern.rb +79 -0
  33. data/lib/aoc_cli/controllers/default_controller.rb +9 -0
  34. data/lib/aoc_cli/controllers/event_controller.rb +35 -0
  35. data/lib/aoc_cli/controllers/help/event_controller.rb +11 -0
  36. data/lib/aoc_cli/controllers/help/puzzle_controller.rb +13 -0
  37. data/lib/aoc_cli/controllers/puzzle_controller.rb +39 -0
  38. data/lib/aoc_cli/core/attempt_parser.rb +69 -0
  39. data/lib/aoc_cli/core/processor.rb +32 -0
  40. data/lib/aoc_cli/core/repository.rb +74 -0
  41. data/lib/aoc_cli/core/request.rb +37 -0
  42. data/lib/aoc_cli/core/resource.rb +39 -0
  43. data/lib/aoc_cli/core/stats_parser.rb +43 -0
  44. data/lib/aoc_cli/helpers/table_generator.rb +64 -0
  45. data/lib/aoc_cli/helpers/view_helper.rb +35 -0
  46. data/lib/aoc_cli/models/attempt.rb +67 -0
  47. data/lib/aoc_cli/models/event.rb +9 -0
  48. data/lib/aoc_cli/models/location.rb +24 -0
  49. data/lib/aoc_cli/models/puzzle.rb +28 -0
  50. data/lib/aoc_cli/models/puzzle_dir_sync_log.rb +14 -0
  51. data/lib/aoc_cli/models/stats.rb +43 -0
  52. data/lib/aoc_cli/presenters/attempt_presenter.rb +29 -0
  53. data/lib/aoc_cli/presenters/puzzle_presenter.rb +29 -0
  54. data/lib/aoc_cli/presenters/stats_presenter.rb +32 -0
  55. data/lib/aoc_cli/processors/event_initialiser.rb +64 -0
  56. data/lib/aoc_cli/processors/puzzle_dir_synchroniser.rb +80 -0
  57. data/lib/aoc_cli/processors/puzzle_initialiser.rb +91 -0
  58. data/lib/aoc_cli/processors/puzzle_refresher.rb +27 -0
  59. data/lib/aoc_cli/processors/resource_attacher.rb +22 -0
  60. data/lib/aoc_cli/processors/solution_poster.rb +72 -0
  61. data/lib/aoc_cli/processors/stats_initialiser.rb +36 -0
  62. data/lib/aoc_cli/processors/stats_refresher.rb +23 -0
  63. data/lib/aoc_cli/validators/collection_type_validator.rb +57 -0
  64. data/lib/aoc_cli/validators/event_year_validator.rb +42 -0
  65. data/lib/aoc_cli/validators/included_validator.rb +21 -0
  66. data/lib/aoc_cli/validators/integer_validator.rb +32 -0
  67. data/lib/aoc_cli/validators/path_validator.rb +39 -0
  68. data/lib/aoc_cli/validators/type_validator.rb +54 -0
  69. data/lib/aoc_cli/version.rb +1 -1
  70. data/lib/aoc_cli/views/event/attach.erb +3 -0
  71. data/lib/aoc_cli/views/event/init.erb +3 -0
  72. data/lib/aoc_cli/views/help/event/attach.erb +32 -0
  73. data/lib/aoc_cli/views/help/event/init.erb +38 -0
  74. data/lib/aoc_cli/views/help/event/progress.erb +12 -0
  75. data/lib/aoc_cli/views/help/puzzle/attempts.erb +12 -0
  76. data/lib/aoc_cli/views/help/puzzle/init.erb +31 -0
  77. data/lib/aoc_cli/views/help/puzzle/solve.erb +33 -0
  78. data/lib/aoc_cli/views/help/puzzle/sync.erb +32 -0
  79. data/lib/aoc_cli/views/puzzle/init.erb +3 -0
  80. data/lib/aoc_cli/views/puzzle/solve.erb +10 -0
  81. data/lib/aoc_cli.rb +32 -16
  82. data/rbs_collection.lock.yaml +168 -0
  83. data/rbs_collection.yaml +28 -0
  84. data/sig/aoc_cli/components/attempts_table.rbs +29 -0
  85. data/sig/aoc_cli/components/docs_component.rbs +15 -0
  86. data/sig/aoc_cli/components/errors_component.rbs +19 -0
  87. data/sig/aoc_cli/components/progress_table.rbs +28 -0
  88. data/sig/aoc_cli/components/puzzle_sync_component.rbs +19 -0
  89. data/sig/aoc_cli/configurators/session_configurator.rbs +7 -0
  90. data/sig/aoc_cli/controllers/application_controller.rbs +12 -0
  91. data/sig/aoc_cli/controllers/concerns/error_concern.rbs +9 -0
  92. data/sig/aoc_cli/controllers/concerns/location_concern.rbs +35 -0
  93. data/sig/aoc_cli/controllers/default_controller.rbs +7 -0
  94. data/sig/aoc_cli/controllers/event_controller.rbs +18 -0
  95. data/sig/aoc_cli/controllers/help/event_controller.rbs +11 -0
  96. data/sig/aoc_cli/controllers/help/puzzle_controller.rbs +13 -0
  97. data/sig/aoc_cli/controllers/puzzle_controller.rbs +15 -0
  98. data/sig/aoc_cli/core/attempt_parser.rbs +41 -0
  99. data/sig/aoc_cli/core/processor.rbs +25 -0
  100. data/sig/aoc_cli/core/repository.rbs +25 -0
  101. data/sig/aoc_cli/core/request.rbs +29 -0
  102. data/sig/aoc_cli/core/resource.rbs +25 -0
  103. data/sig/aoc_cli/core/stats_parser.rbs +23 -0
  104. data/sig/aoc_cli/helpers/table_generator.rbs +36 -0
  105. data/sig/aoc_cli/helpers/view_helper.rbs +15 -0
  106. data/sig/aoc_cli/models/attempt.rbs +35 -0
  107. data/sig/aoc_cli/models/event.rbs +17 -0
  108. data/sig/aoc_cli/models/location.rbs +19 -0
  109. data/sig/aoc_cli/models/puzzle.rbs +28 -0
  110. data/sig/aoc_cli/models/puzzle_dir_sync_log.rbs +11 -0
  111. data/sig/aoc_cli/models/stats.rbs +53 -0
  112. data/sig/aoc_cli/presenters/attempt_presenter.rbs +13 -0
  113. data/sig/aoc_cli/presenters/puzzle_presenter.rbs +19 -0
  114. data/sig/aoc_cli/presenters/stats_presenter.rbs +19 -0
  115. data/sig/aoc_cli/processors/event_initialiser.rbs +26 -0
  116. data/sig/aoc_cli/processors/puzzle_dir_synchroniser.rbs +40 -0
  117. data/sig/aoc_cli/processors/puzzle_initialiser.rbs +37 -0
  118. data/sig/aoc_cli/processors/puzzle_refresher.rbs +21 -0
  119. data/sig/aoc_cli/processors/resource_attacher.rbs +16 -0
  120. data/sig/aoc_cli/processors/solution_poster.rbs +34 -0
  121. data/sig/aoc_cli/processors/stats_initialiser.rbs +15 -0
  122. data/sig/aoc_cli/processors/stats_refresher.rbs +19 -0
  123. data/sig/aoc_cli/validators/collection_type_validator.rbs +24 -0
  124. data/sig/aoc_cli/validators/event_year_validator.rbs +19 -0
  125. data/sig/aoc_cli/validators/included_validator.rbs +11 -0
  126. data/sig/aoc_cli/validators/integer_validator.rbs +15 -0
  127. data/sig/aoc_cli/validators/path_validator.rbs +17 -0
  128. data/sig/aoc_cli/validators/type_validator.rbs +22 -0
  129. data/sig/aoc_cli.rbs +6 -0
  130. data/sig/http.rbs +3 -0
  131. data/sig/kangaru.rbs +5 -0
  132. data/sig/nokogiri.rbs +3 -0
  133. data/sig/reverse_markdown.rbs +3 -0
  134. metadata +142 -34
  135. data/.gitignore +0 -5
  136. data/bin/aoc +0 -4
  137. data/bin/console +0 -15
  138. data/bin/setup +0 -7
  139. data/lib/aoc_cli/commands.rb +0 -232
  140. data/lib/aoc_cli/database.rb +0 -224
  141. data/lib/aoc_cli/day.rb +0 -124
  142. data/lib/aoc_cli/db/reddit.db +0 -0
  143. data/lib/aoc_cli/errors.rb +0 -275
  144. data/lib/aoc_cli/files.rb +0 -163
  145. data/lib/aoc_cli/help.rb +0 -77
  146. data/lib/aoc_cli/interface.rb +0 -81
  147. data/lib/aoc_cli/paths.rb +0 -101
  148. data/lib/aoc_cli/solve.rb +0 -104
  149. data/lib/aoc_cli/tables.rb +0 -138
  150. data/lib/aoc_cli/tools.rb +0 -120
  151. data/lib/aoc_cli/year.rb +0 -116
  152. 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