diff --git a/Gemfile.lock b/Gemfile.lock
index 934f84c..5b63ee5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -52,6 +52,7 @@ GEM
PLATFORMS
arm64-darwin-21
+ ruby
DEPENDENCIES
advent!
diff --git a/README.md b/README.md
index 6165886..d494cd7 100644
--- a/README.md
+++ b/README.md
@@ -16,15 +16,26 @@ If bundler is not being used to manage dependencies, install the gem by executin
## Usage
+Initialise a new project somewhere:
+
+```bash
+mkdir advent_of_code && cd advent_of_code
+
+# create a blank advent.yml config file
+advent init
+```
+
+Configuration values and format are explained in the [Config](#config) section.
+
Advent expects you to have a working directory resembling something like:
$ tree
.
├── 2015
- └── 2016
+ ├── 2016
+ └── advent.yml
-Some commands can be run from within a directory for a specific year, but it's
-better to run from the parent directory where possible.
+You can run commands from anywhere under this directory.
The typical flow for tackling a daily challenge would be:
@@ -40,6 +51,32 @@ A list of commands and help is available using `advent`:
$ advent help
+## Config
+
+The config file should be at the root of your working directory called
+`advent.yml`. The default values if you don't provide an override are:
+
+```yaml
+download_when_generating: true
+remember_session: true
+```
+
+### Config explained
+
+
+- download_when_generating
+-
+When you run `advent generate` it will automatically download the input file to
+go with it
+
+
+- remember_session
+-
+Save your session cookie in `.advent_session` when prompted so you don't need to
+find it again
+
+
+
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
diff --git a/advent.gemspec b/advent.gemspec
index bb250d6..7a8ec47 100644
--- a/advent.gemspec
+++ b/advent.gemspec
@@ -19,4 +19,12 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]
spec.add_dependency "thor", "~> 1.2"
+
+ spec.post_install_message = "
+advent v0.1.5 requires a config file in your working directory.
+
+See #{spec.homepage}/blob/main/README.md#usage or if you're
+brave run `advent init` in your current directory.
+
+"
end
diff --git a/lib/advent.rb b/lib/advent.rb
index 3e8ab1f..c74411a 100644
--- a/lib/advent.rb
+++ b/lib/advent.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require_relative "advent/configuration"
require_relative "advent/input"
require_relative "advent/session"
require_relative "advent/solution"
@@ -10,8 +11,29 @@ module Advent
class Error < StandardError; end
class << self
+ def config
+ @_config ||= Configuration.from_file(root.join(Configuration::FILE_NAME))
+ end
+
+ def root
+ if (location = find_config_location)
+ location
+ else
+ raise Error, "Cannot find advent.yml config file in current or parent directories."
+ end
+ end
+
def session
- @_session = Session.new
+ @_session ||= Session.new
+ end
+
+ private
+
+ def find_config_location
+ Pathname.new(Dir.pwd).ascend do |path|
+ return path if File.exist? path.join(Configuration::FILE_NAME)
+ return nil if path.to_s == "/"
+ end
end
end
end
diff --git a/lib/advent/cli.rb b/lib/advent/cli.rb
index 8b6a1b5..60bb56b 100644
--- a/lib/advent/cli.rb
+++ b/lib/advent/cli.rb
@@ -9,14 +9,17 @@ module Advent
class CLI < Thor
include Thor::Actions
- class_option :root_path, default: Dir.pwd, hide: true, check_default_type: false
class_option :http_module, default: Net::HTTP, check_default_type: false
def initialize(*args)
super
- self.destination_root = root_path
source_paths << File.expand_path("templates", __dir__)
+
+ # Don't try to load Advent.root if we're running init
+ unless args.last[:current_command]&.name == "init"
+ self.destination_root = Advent.root
+ end
end
# @return [Boolean] defines whether an exit status is set if a command fails
@@ -24,44 +27,12 @@ def self.exit_on_failure?
true
end
- no_commands do
- # @return [Boolean] whether the current root_path option is in a
- # directory that looks like a year (eg. 2015)
- def in_year_directory?
- dir = root_path.basename.to_s
- dir =~ /^20[0-9]{2}/
- end
- end
-
desc "download YEAR DAY", "Download the input for YEAR and DAY"
- def download(year_or_day, day = nil)
- year, day = determine_year_and_day(year_or_day, day)
-
- if (error_message = validate(year, day))
- say_error error_message, :red
- return
- end
+ def download(year, day)
+ require "advent/cli/downloader"
- subpath = if in_year_directory?
- ""
- else
- "#{year}/"
- end
-
- unless Advent.session.exist?
- session = ask "What is your Advent of Code session cookie value?", echo: false
- Advent.session.value = session
-
- say "\n\nThanks. Psst, we're going to save this for next time. It's in .advent_session if you need to update or delete it.\n\n"
- end
-
- input = Advent::Input.new(root_path.join(subpath), day: day.to_i)
-
- if input.download(Advent.session.value, options.http_module)
- say "Input downloaded to #{input.file_path}.", :green
- say "\nUsing #load_input in your daily solution will load the input file for you."
- else
- say_error "Something went wrong, maybe an old session cookie?", :red
+ Dir.chdir Advent.root do
+ Downloader.new(self, year, day).download
end
end
@@ -69,29 +40,37 @@ def download(year_or_day, day = nil)
# Generates a new solution file. If within a year directory, only the day
# is used, otherwise both the year and day will be required to generate the
# output.
- def generate(year_or_day, day = nil)
- year, day = determine_year_and_day(year_or_day, day)
+ def generate(year, day)
+ year = parse_number year
+ day = parse_number day
- if (error_message = validate(year, day))
- say_error error_message, :red
+ if (message = validate(year, day))
+ say_error message, :red
return
end
- subpath = if in_year_directory?
+ template "solution.rb.tt", "#{year}/day#{day}.rb", context: binding
+ template "solution_test.rb.tt", "#{year}/test/day#{day}_test.rb", context: binding
+
+ download year, day if Advent.config.download_when_generating
+ end
+
+ desc "init DIR", "Initialise a new advent project in DIR"
+ def init(dir = ".")
+ create_file Pathname.getwd.join(dir).join(Advent::Configuration::FILE_NAME) do
""
- else
- "#{year}/"
end
-
- template "solution.rb.tt", "#{subpath}day#{day}.rb", context: binding
- template "solution_test.rb.tt", "#{subpath}test/day#{day}_test.rb", context: binding
end
desc "solve FILE", "Solve your solution"
# Runs a solution file, outputting both :part1 and :part2 method return values.
def solve(path)
require "advent/cli/solver"
- Solver.new(self, root_path.join(path)).solve
+ file_path = Pathname.getwd.join(path)
+
+ Dir.chdir Advent.root do
+ Solver.new(self, file_path.relative_path_from(Advent.root)).solve
+ end
end
desc "version", "Prints the current version of the gem"
@@ -102,22 +81,6 @@ def version
private
- def determine_year_and_day(year_or_day, day)
- if in_year_directory?
- [root_path.basename.to_s, parse_number(year_or_day)]
- else
- [year_or_day, parse_number(day)]
- end
- end
-
- def root_path
- @_root_path ||= if options.root_path.is_a?(Pathname)
- options.root_path
- else
- Pathname.new(options.root_path)
- end
- end
-
def parse_number(str)
if (m = str.match(/[0-9]+/))
m[0]
diff --git a/lib/advent/cli/downloader.rb b/lib/advent/cli/downloader.rb
new file mode 100644
index 0000000..9c88277
--- /dev/null
+++ b/lib/advent/cli/downloader.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class Advent::CLI::Downloader
+ def initialize(command, year, day)
+ @command = command
+ @year = year
+ @day = day
+ end
+
+ def download
+ ask_for_session_cookie_if_needed
+ input = Advent::Input.new(Advent.root.join(@year), day: @day.to_i)
+
+ if input.download(Advent.session.value, @command.options.http_module)
+ @command.say "Input downloaded to #{input.file_path}.", :green
+ @command.say "\nUsing #load_input in your daily solution will load the input file for you."
+ else
+ @command.say_error "Something went wrong, maybe an old session cookie?", :red
+ end
+ end
+
+ private
+
+ def ask_for_session_cookie_if_needed
+ return if Advent.session.exist?
+
+ session = @command.ask "What is your Advent of Code session cookie value?", echo: false
+ Advent.session.value = session
+
+ @command.say "\n\nThanks. Psst, we're going to save this for next time. It's in .advent_session if you need to update or delete it.\n\n"
+ end
+end
diff --git a/lib/advent/cli/solver.rb b/lib/advent/cli/solver.rb
index f347351..a45e376 100644
--- a/lib/advent/cli/solver.rb
+++ b/lib/advent/cli/solver.rb
@@ -18,7 +18,7 @@ def solve
load @path, Solutions
solution = Solutions.const_get(solution_class_name).new
else
- require @path
+ require @path.expand_path
solution = Object.const_get(solution_class_name).new
end
@@ -37,15 +37,11 @@ def solve
private
- def day
- @_day ||= @path.basename.to_s.match(/day([0-9]+)\.rb/)[1]
- end
-
- def solution_file_name
- "day#{day}.rb"
- end
-
def solution_class_name
"Day#{day}"
end
+
+ def day
+ @_day ||= @path.basename.to_s.match(/day([0-9]+)\.rb/)[1]
+ end
end
diff --git a/lib/advent/configuration.rb b/lib/advent/configuration.rb
new file mode 100644
index 0000000..90d246c
--- /dev/null
+++ b/lib/advent/configuration.rb
@@ -0,0 +1,30 @@
+require "psych"
+
+module Advent
+ class Configuration
+ DEFAULTS = {
+ "download_when_generating" => true,
+ "remember_session" => true
+ }
+ FILE_NAME = "advent.yml"
+
+ attr_reader :download_when_generating, :remember_session
+
+ class << self
+ def from_file(file = FILE_NAME)
+ if RUBY_VERSION >= "3.1"
+ new Psych.safe_load_file(file)
+ else
+ new Psych.safe_load(File.read(file))
+ end
+ end
+ end
+
+ def initialize(conf)
+ config = DEFAULTS.merge(conf || {})
+
+ @download_when_generating = config.dig("download_when_generating")
+ @remember_session = config.dig("remember_session")
+ end
+ end
+end
diff --git a/lib/advent/session.rb b/lib/advent/session.rb
index e2c2f15..348b973 100644
--- a/lib/advent/session.rb
+++ b/lib/advent/session.rb
@@ -18,11 +18,22 @@ def exist?
end
def value=(val)
- File.write file_name, val
+ if save_to_disk?
+ File.write file_name, val
+ else
+ @_value = val
+ end
end
def value
- File.read file_name if exist?
+ return @_value unless save_to_disk?
+ return File.read(file_name) if exist?
+ end
+
+ private
+
+ def save_to_disk?
+ Advent.config.remember_session
end
end
end
diff --git a/test/advent/cli_test.rb b/test/advent/cli_test.rb
index 66128b2..4be6c2f 100644
--- a/test/advent/cli_test.rb
+++ b/test/advent/cli_test.rb
@@ -26,8 +26,29 @@ def setup
"day 5 input"
)
- @cli = Advent::CLI.new([], root_path: DUMMY_ROOT_PATH, http_module: http_mock)
- @year_cli = Advent::CLI.new([], root_path: DUMMY_ROOT_PATH.join("2015"))
+ Dir.chdir DUMMY_ROOT_PATH
+ @cli = Advent::CLI.new([], http_module: http_mock)
+ end
+
+ def teardown
+ Dir.chdir DUMMY_ROOT_PATH
+ Advent.session.clear
+ end
+
+ def test_init
+ path = DUMMY_ROOT_PATH.join("init")
+ Dir.mkdir path
+
+ out, _err = capture_io do
+ Dir.chdir path do
+ @cli.invoke(:init)
+ end
+ end
+
+ assert_match(/create.*advent.yml/, out.strip)
+ ensure
+ File.delete path.join("advent.yml")
+ Dir.rmdir path
end
def test_version
@@ -38,7 +59,7 @@ def test_version
assert_equal Advent::VERSION, out.strip
end
- def test_solve_from_parent_directory
+ def test_solve_from_root_directory
out, _err = capture_io do
@cli.invoke(:solve, ["2015/day1.rb"])
end
@@ -48,13 +69,15 @@ def test_solve_from_parent_directory
def test_solve_from_year_directory
out, _err = capture_io do
- @year_cli.invoke(:solve, ["day2.rb"])
+ Dir.chdir DUMMY_ROOT_PATH.join("2015") do
+ @cli.invoke(:solve, ["day2.rb"])
+ end
end
assert_equal "Part 1: 789\nPart 2: Missing", out.strip
end
- def test_generate_solution_with_year_and_day
+ def test_generate
capture_io do
@cli.invoke(:generate, ["2015", "3"])
end
@@ -67,22 +90,26 @@ def test_generate_solution_with_year_and_day
end
end
- def test_generate_solution_from_year_directory_with_day
- capture_io do
- @year_cli.invoke(:generate, ["3"])
+ def test_download_when_generating
+ with_session("abc123") do
+ with_config({"download_when_generating" => true}) do
+ capture_io do
+ @cli.invoke(:generate, ["2015", "3"])
+ end
+ end
end
- ["day3.rb", "test/day3_test.rb"].each do |file_name|
- expected_file_path = DUMMY_ROOT_PATH.join("2015", file_name)
- assert File.exist? expected_file_path
+ assert File.exist? DUMMY_ROOT_PATH.join("2015", "day3.rb")
+ assert File.exist? DUMMY_ROOT_PATH.join("2015", ".day3.input.txt")
- File.delete expected_file_path
- end
+ File.delete DUMMY_ROOT_PATH.join("2015", "day3.rb")
+ File.delete DUMMY_ROOT_PATH.join("2015", "test", "day3_test.rb")
+ File.delete DUMMY_ROOT_PATH.join("2015", ".day3.input.txt")
end
- def test_generate_solution_with_non_numbers_in_day
+ def test_generate_day_parsing
capture_io do
- @year_cli.invoke(:generate, ["day3"])
+ @cli.invoke(:generate, ["2015", "day3"])
end
["day3.rb", "test/day3_test.rb"].each do |file_name|
@@ -93,7 +120,7 @@ def test_generate_solution_with_non_numbers_in_day
end
end
- def test_generate_solution_valid_minimum_year
+ def test_generate_valid_minimum_year
_out, err = capture_io do
@cli.invoke(:generate, ["2013", "1"])
end
@@ -101,15 +128,15 @@ def test_generate_solution_valid_minimum_year
assert_equal "Advent of Code only started in 2014!", err.strip
end
- def test_generate_solution_valid_maximum_year
+ def test_generate_valid_maximum_year
_out, err = capture_io do
- @cli.invoke(:generate, [Date.today.year + 1, "1"])
+ @cli.invoke(:generate, [(Date.today.year + 1).to_s, "1"])
end
assert_equal "Future years are not supported.", err.strip
end
- def test_generate_solution_valid_minimum_day
+ def test_generate_valid_minimum_day
_out, err = capture_io do
@cli.invoke(:generate, ["2015", "0"])
end
@@ -117,7 +144,7 @@ def test_generate_solution_valid_minimum_day
assert_equal "Day must be between 1 and 25 (inclusive).", err.strip
end
- def test_generate_solution_valid_maximum_day
+ def test_generate_valid_maximum_day
_out, err = capture_io do
@cli.invoke(:generate, ["2015", "26"])
end
@@ -125,9 +152,9 @@ def test_generate_solution_valid_maximum_day
assert_equal "Day must be between 1 and 25 (inclusive).", err.strip
end
- def test_download_input
+ def test_download_from_root_directory
out, _err = capture_io do
- with_stdin_input(@session) do
+ with_session(@session) do
@cli.invoke(:download, ["2015", "3"])
end
end
@@ -137,10 +164,10 @@ def test_download_input
assert_equal "day 3 input", File.read(input_path)
assert_match(/Input downloaded to #{input_path}/, out.strip)
ensure
- File.delete input_path
+ File.delete input_path if File.exist? input_path
end
- def test_download_persists_session_cookie
+ def test_persisting_session_cookie
with_stdin_input(@session) do
capture_io do
@cli.invoke(:download, ["2015", "4"])
@@ -149,11 +176,11 @@ def test_download_persists_session_cookie
assert_equal @session, Advent.session.value
ensure
- File.delete DUMMY_ROOT_PATH.join("2015", ".day4.input.txt")
- Advent.session.clear
+ input = DUMMY_ROOT_PATH.join("2015", ".day4.input.txt")
+ File.delete input if File.exist? input
end
- def test_download_skips_asking_for_session_cookie
+ def test_not_asking_for_session_cookie_again
Advent.session.value = @session
out, _err = capture_io do
@@ -164,22 +191,20 @@ def test_download_skips_asking_for_session_cookie
refute_match(/session cookie value/, out)
ensure
- File.delete DUMMY_ROOT_PATH.join("2015", ".day5.input.txt")
- Advent.session.clear
+ input = DUMMY_ROOT_PATH.join("2015", ".day5.input.txt")
+ File.delete input if File.exist? input
end
- def test_download_input_failure
+ def test_download_error
_out, err = capture_io do
- with_stdin_input("invalid") do
+ with_session("invalid") do
@cli.invoke(:download, ["2015", "6"])
end
end
- input_path = DUMMY_ROOT_PATH.join("2015", ".day6.input.txt")
- refute File.exist?(input_path)
assert_match(/Something went wrong/, err.strip)
ensure
- File.delete input_path if File.exist? input_path
- Advent.session.clear
+ input = DUMMY_ROOT_PATH.join("2015", ".day6.input.txt")
+ File.delete input_path if File.exist? input
end
end
diff --git a/test/advent/configuration_test.rb b/test/advent/configuration_test.rb
new file mode 100644
index 0000000..8f11e77
--- /dev/null
+++ b/test/advent/configuration_test.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class Advent::ConfigurationTest < Advent::TestCase
+ def setup
+ @config_file = DUMMY_ROOT_PATH.join("advent.yml")
+ end
+
+ def test_config_is_merged_with_defaults
+ config = Advent::Configuration.new({})
+
+ assert config.download_when_generating
+ assert config.remember_session
+ end
+
+ def test_initializing_from_file
+ config = Advent::Configuration.from_file(@config_file)
+
+ refute config.download_when_generating
+ assert config.remember_session
+ end
+end
diff --git a/test/advent/session_test.rb b/test/advent/session_test.rb
index bec1ba3..92f6ecc 100644
--- a/test/advent/session_test.rb
+++ b/test/advent/session_test.rb
@@ -2,6 +2,7 @@
class Advent::SessionTest < Advent::TestCase
def setup
+ Dir.chdir DUMMY_ROOT_PATH
@session = Advent::Session.new(name)
end
@@ -26,6 +27,15 @@ def test_setting_value
assert_equal "session value", File.read(@session.file_name)
end
+ def test_setting_value_honouring_config
+ with_config({"remember_session" => false}) do
+ @session.value = "session value"
+
+ assert_equal "session value", @session.value
+ refute @session.exist?
+ end
+ end
+
def test_value
assert_nil @session.value
File.write @session.file_name, "another session value"
diff --git a/test/advent_test.rb b/test/advent_test.rb
index 46b6ea8..6de001c 100644
--- a/test/advent_test.rb
+++ b/test/advent_test.rb
@@ -3,6 +3,34 @@
require "test_helper"
class TestAdvent < Advent::TestCase
+ def teardown
+ Dir.chdir DUMMY_ROOT_PATH
+ end
+
+ def test_finding_root_from_config_location
+ Dir.chdir DUMMY_ROOT_PATH do
+ assert_equal DUMMY_ROOT_PATH, Advent.root
+ end
+
+ Dir.chdir DUMMY_ROOT_PATH.join("2015") do
+ assert_equal DUMMY_ROOT_PATH, Advent.root
+ end
+
+ Dir.chdir DUMMY_ROOT_PATH.join("..") do
+ error = assert_raises Advent::Error do
+ Advent.root
+ end
+
+ assert_equal "Cannot find advent.yml config file in current or parent directories.", error.message
+ end
+ end
+
+ def test_config_initialization
+ Dir.chdir DUMMY_ROOT_PATH do
+ assert_kind_of Advent::Configuration, Advent.config
+ end
+ end
+
def test_that_it_has_a_version_number
refute_nil ::Advent::VERSION
end
diff --git a/test/dummy/advent.yml b/test/dummy/advent.yml
new file mode 100644
index 0000000..e89a55d
--- /dev/null
+++ b/test/dummy/advent.yml
@@ -0,0 +1,2 @@
+download_when_generating: false
+remember_session: true
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 901ce7f..ee8e04d 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -8,6 +8,24 @@
DUMMY_ROOT_PATH = Pathname.new File.expand_path("dummy", __dir__)
class Advent::TestCase < Minitest::Test
+ def with_config(config)
+ original_config = Advent.config
+
+ config_with_defaults = Advent::Configuration::DEFAULTS.merge(config)
+ Advent.instance_variable_set(:@_config, Advent::Configuration.new(config_with_defaults))
+
+ yield
+ ensure
+ Advent.instance_variable_set(:@_config, original_config)
+ end
+
+ def with_session(value)
+ Advent.session.value = value
+ yield
+ ensure
+ Advent.session.clear
+ end
+
def with_stdin_input(input)
require "stringio"