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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +88 -0
- data/.ruby-version +1 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +187 -0
- data/README.md +88 -282
- data/Rakefile +9 -2
- data/Steepfile +13 -0
- data/aoc_cli.gemspec +36 -26
- data/db/migrate/1_create_events.rb +14 -0
- data/db/migrate/2_create_puzzles.rb +21 -0
- data/db/migrate/3_create_stats.rb +19 -0
- data/db/migrate/4_create_attempts.rb +19 -0
- data/db/migrate/5_create_locations.rb +17 -0
- data/db/migrate/6_create_puzzle_dir_sync_logs.rb +18 -0
- data/exe/aoc +5 -0
- data/lib/aoc_cli/components/attempts_table.erb +5 -0
- data/lib/aoc_cli/components/attempts_table.rb +58 -0
- data/lib/aoc_cli/components/docs_component.erb +19 -0
- data/lib/aoc_cli/components/docs_component.rb +41 -0
- data/lib/aoc_cli/components/errors_component.erb +8 -0
- data/lib/aoc_cli/components/errors_component.rb +36 -0
- data/lib/aoc_cli/components/progress_table.erb +1 -0
- data/lib/aoc_cli/components/progress_table.rb +43 -0
- data/lib/aoc_cli/components/puzzle_sync_component.erb +2 -0
- data/lib/aoc_cli/components/puzzle_sync_component.rb +26 -0
- data/lib/aoc_cli/configurators/session_configurator.rb +7 -0
- data/lib/aoc_cli/controllers/application_controller.rb +27 -0
- data/lib/aoc_cli/controllers/concerns/error_concern.rb +17 -0
- data/lib/aoc_cli/controllers/concerns/location_concern.rb +79 -0
- data/lib/aoc_cli/controllers/default_controller.rb +9 -0
- data/lib/aoc_cli/controllers/event_controller.rb +35 -0
- data/lib/aoc_cli/controllers/help/event_controller.rb +11 -0
- data/lib/aoc_cli/controllers/help/puzzle_controller.rb +13 -0
- data/lib/aoc_cli/controllers/puzzle_controller.rb +39 -0
- data/lib/aoc_cli/core/attempt_parser.rb +69 -0
- data/lib/aoc_cli/core/processor.rb +32 -0
- data/lib/aoc_cli/core/repository.rb +74 -0
- data/lib/aoc_cli/core/request.rb +37 -0
- data/lib/aoc_cli/core/resource.rb +39 -0
- data/lib/aoc_cli/core/stats_parser.rb +43 -0
- data/lib/aoc_cli/helpers/table_generator.rb +64 -0
- data/lib/aoc_cli/helpers/view_helper.rb +35 -0
- data/lib/aoc_cli/models/attempt.rb +67 -0
- data/lib/aoc_cli/models/event.rb +9 -0
- data/lib/aoc_cli/models/location.rb +24 -0
- data/lib/aoc_cli/models/puzzle.rb +28 -0
- data/lib/aoc_cli/models/puzzle_dir_sync_log.rb +14 -0
- data/lib/aoc_cli/models/stats.rb +43 -0
- data/lib/aoc_cli/presenters/attempt_presenter.rb +29 -0
- data/lib/aoc_cli/presenters/puzzle_presenter.rb +29 -0
- data/lib/aoc_cli/presenters/stats_presenter.rb +32 -0
- data/lib/aoc_cli/processors/event_initialiser.rb +64 -0
- data/lib/aoc_cli/processors/puzzle_dir_synchroniser.rb +80 -0
- data/lib/aoc_cli/processors/puzzle_initialiser.rb +91 -0
- data/lib/aoc_cli/processors/puzzle_refresher.rb +27 -0
- data/lib/aoc_cli/processors/resource_attacher.rb +22 -0
- data/lib/aoc_cli/processors/solution_poster.rb +72 -0
- data/lib/aoc_cli/processors/stats_initialiser.rb +36 -0
- data/lib/aoc_cli/processors/stats_refresher.rb +23 -0
- data/lib/aoc_cli/validators/collection_type_validator.rb +57 -0
- data/lib/aoc_cli/validators/event_year_validator.rb +42 -0
- data/lib/aoc_cli/validators/included_validator.rb +21 -0
- data/lib/aoc_cli/validators/integer_validator.rb +32 -0
- data/lib/aoc_cli/validators/path_validator.rb +39 -0
- data/lib/aoc_cli/validators/type_validator.rb +54 -0
- data/lib/aoc_cli/version.rb +1 -1
- data/lib/aoc_cli/views/event/attach.erb +3 -0
- data/lib/aoc_cli/views/event/init.erb +3 -0
- data/lib/aoc_cli/views/help/event/attach.erb +32 -0
- data/lib/aoc_cli/views/help/event/init.erb +38 -0
- data/lib/aoc_cli/views/help/event/progress.erb +12 -0
- data/lib/aoc_cli/views/help/puzzle/attempts.erb +12 -0
- data/lib/aoc_cli/views/help/puzzle/init.erb +31 -0
- data/lib/aoc_cli/views/help/puzzle/solve.erb +33 -0
- data/lib/aoc_cli/views/help/puzzle/sync.erb +32 -0
- data/lib/aoc_cli/views/puzzle/init.erb +3 -0
- data/lib/aoc_cli/views/puzzle/solve.erb +10 -0
- data/lib/aoc_cli.rb +32 -16
- data/rbs_collection.lock.yaml +168 -0
- data/rbs_collection.yaml +28 -0
- data/sig/aoc_cli/components/attempts_table.rbs +29 -0
- data/sig/aoc_cli/components/docs_component.rbs +15 -0
- data/sig/aoc_cli/components/errors_component.rbs +19 -0
- data/sig/aoc_cli/components/progress_table.rbs +28 -0
- data/sig/aoc_cli/components/puzzle_sync_component.rbs +19 -0
- data/sig/aoc_cli/configurators/session_configurator.rbs +7 -0
- data/sig/aoc_cli/controllers/application_controller.rbs +12 -0
- data/sig/aoc_cli/controllers/concerns/error_concern.rbs +9 -0
- data/sig/aoc_cli/controllers/concerns/location_concern.rbs +35 -0
- data/sig/aoc_cli/controllers/default_controller.rbs +7 -0
- data/sig/aoc_cli/controllers/event_controller.rbs +18 -0
- data/sig/aoc_cli/controllers/help/event_controller.rbs +11 -0
- data/sig/aoc_cli/controllers/help/puzzle_controller.rbs +13 -0
- data/sig/aoc_cli/controllers/puzzle_controller.rbs +15 -0
- data/sig/aoc_cli/core/attempt_parser.rbs +41 -0
- data/sig/aoc_cli/core/processor.rbs +25 -0
- data/sig/aoc_cli/core/repository.rbs +25 -0
- data/sig/aoc_cli/core/request.rbs +29 -0
- data/sig/aoc_cli/core/resource.rbs +25 -0
- data/sig/aoc_cli/core/stats_parser.rbs +23 -0
- data/sig/aoc_cli/helpers/table_generator.rbs +36 -0
- data/sig/aoc_cli/helpers/view_helper.rbs +15 -0
- data/sig/aoc_cli/models/attempt.rbs +35 -0
- data/sig/aoc_cli/models/event.rbs +17 -0
- data/sig/aoc_cli/models/location.rbs +19 -0
- data/sig/aoc_cli/models/puzzle.rbs +28 -0
- data/sig/aoc_cli/models/puzzle_dir_sync_log.rbs +11 -0
- data/sig/aoc_cli/models/stats.rbs +53 -0
- data/sig/aoc_cli/presenters/attempt_presenter.rbs +13 -0
- data/sig/aoc_cli/presenters/puzzle_presenter.rbs +19 -0
- data/sig/aoc_cli/presenters/stats_presenter.rbs +19 -0
- data/sig/aoc_cli/processors/event_initialiser.rbs +26 -0
- data/sig/aoc_cli/processors/puzzle_dir_synchroniser.rbs +40 -0
- data/sig/aoc_cli/processors/puzzle_initialiser.rbs +37 -0
- data/sig/aoc_cli/processors/puzzle_refresher.rbs +21 -0
- data/sig/aoc_cli/processors/resource_attacher.rbs +16 -0
- data/sig/aoc_cli/processors/solution_poster.rbs +34 -0
- data/sig/aoc_cli/processors/stats_initialiser.rbs +15 -0
- data/sig/aoc_cli/processors/stats_refresher.rbs +19 -0
- data/sig/aoc_cli/validators/collection_type_validator.rbs +24 -0
- data/sig/aoc_cli/validators/event_year_validator.rbs +19 -0
- data/sig/aoc_cli/validators/included_validator.rbs +11 -0
- data/sig/aoc_cli/validators/integer_validator.rbs +15 -0
- data/sig/aoc_cli/validators/path_validator.rbs +17 -0
- data/sig/aoc_cli/validators/type_validator.rbs +22 -0
- data/sig/aoc_cli.rbs +6 -0
- data/sig/http.rbs +3 -0
- data/sig/kangaru.rbs +5 -0
- data/sig/nokogiri.rbs +3 -0
- data/sig/reverse_markdown.rbs +3 -0
- metadata +142 -34
- data/.gitignore +0 -5
- data/bin/aoc +0 -4
- data/bin/console +0 -15
- data/bin/setup +0 -7
- data/lib/aoc_cli/commands.rb +0 -232
- data/lib/aoc_cli/database.rb +0 -224
- data/lib/aoc_cli/day.rb +0 -124
- data/lib/aoc_cli/db/reddit.db +0 -0
- data/lib/aoc_cli/errors.rb +0 -275
- data/lib/aoc_cli/files.rb +0 -163
- data/lib/aoc_cli/help.rb +0 -77
- data/lib/aoc_cli/interface.rb +0 -81
- data/lib/aoc_cli/paths.rb +0 -101
- data/lib/aoc_cli/solve.rb +0 -104
- data/lib/aoc_cli/tables.rb +0 -138
- data/lib/aoc_cli/tools.rb +0 -120
- data/lib/aoc_cli/year.rb +0 -116
- 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,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
|