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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +88 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +4 -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
|