Skip to content

Commit

Permalink
Merge pull request #132 from rodjek/answer_file
Browse files Browse the repository at this point in the history
(SDK-305) Answer file to cache module interview answers, template-url etc
  • Loading branch information
bmjen authored Jul 13, 2017
2 parents d379920 + 093f947 commit 0621158
Show file tree
Hide file tree
Showing 8 changed files with 847 additions and 90 deletions.
1 change: 1 addition & 0 deletions lib/pdk.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require 'pdk/answer_file'
require 'pdk/generate'
require 'pdk/i18n'
require 'pdk/logger'
Expand Down
120 changes: 120 additions & 0 deletions lib/pdk/answer_file.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
require 'json'

module PDK
# Singleton accessor to the current answer file being used by the PDK.
#
# @return [PDK::AnswerFile] The AnswerFile instance currently being used by
# the PDK.
def self.answers
@answer_file ||= PDK::AnswerFile.new
end

# Specify the path to a custom answer file that the PDK should use.
#
# @param path [String] A path on disk to the file where the PDK should store
# answers to interactive questions.
def self.answer_file=(value)
@answer_file = PDK::AnswerFile.new(value)
end

class AnswerFile
attr_reader :answers
attr_reader :answer_file_path

# Initialises the AnswerFile object, which stores the responses to certain
# interactive questions.
#
# @param answer_file_path [String, nil] The path on disk to the file where
# the answers will be stored and read from. If not specified (or `nil`),
# the default path will be used (see #default_answer_file_path).
#
# @raise (see #read_from_disk)
def initialize(answer_file_path = nil)
@answer_file_path = answer_file_path || default_answer_file_path
@answers = read_from_disk
end

# Retrieve the stored answer to a question.
#
# @param question [String] The question name/identifying string.
#
# @return [Object] The answer to the question, or `nil` if no answer found.
def [](question)
answers[question]
end

# Update the stored answers in memory and then save them to disk.
#
# @param new_answers [Hash{String => Object}] The new questions and answers
# to be merged into the existing answers.
#
# @raise [PDK::CLI::FatalError] if the new answers are not provided as
# a Hash.
# @raise (see #save_to_disk)
def update!(new_answers = {})
unless new_answers.is_a?(Hash)
raise PDK::CLI::FatalError, _('Answer file can only be updated with a Hash')
end

answers.merge!(new_answers)

save_to_disk
end

private

# Determine the default path to the answer file.
#
# @return [String] The path on disk to the default answer file.
def default_answer_file_path
File.join(PDK::Util.cachedir, 'answers.json')
end

# Read existing answers into memory from the answer file on disk.
#
# @raise [PDK::CLI::FatalError] If the answer file exists but can not be
# read.
#
# @return [Hash{String => Object}] The existing questions and answers.
def read_from_disk
return {} if !File.file?(answer_file_path) || File.zero?(answer_file_path)

unless File.readable?(answer_file_path)
raise PDK::CLI::FatalError, _("Unable to open '%{file}' for reading") % {
file: answer_file_path,
}
end

answers = JSON.parse(File.read(answer_file_path))
if answers.is_a?(Hash)
answers
else
PDK.logger.warn _("Answer file '%{path}' did not contain a valid set of answers, recreating it") % {
path: answer_file_path,
}
{}
end
rescue JSON::JSONError
PDK.logger.warn _("Answer file '%{path}' did not contain valid JSON, recreating it") % {
path: answer_file_path,
}
{}
end

# Save the in memory answer set to the answer file on disk.
#
# @raise [PDK::CLI::FatalError] if the answer file can not be written to.
def save_to_disk
FileUtils.mkdir_p(File.dirname(answer_file_path))

File.open(answer_file_path, 'w') do |answer_file|
answer_file.puts JSON.pretty_generate(answers)
end
rescue SystemCallError, IOError => e
raise PDK::CLI::FatalError, _("Unable to write '%{file}': %{msg}") % {
file: answer_file_path,
msg: e.message,
}
end
end
end
4 changes: 4 additions & 0 deletions lib/pdk/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ def self.template_url_option(dsl)
flag :d, :debug, _('Enable debug output.') do |_, _|
PDK.logger.enable_debug_output
end

option nil, 'answer-file', _('Path to an answer file'), argument: :required, hidden: true do |value|
PDK.answer_file = value
end
end

require 'pdk/cli/new'
Expand Down
8 changes: 4 additions & 4 deletions lib/pdk/cli/util/interview.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ def run
num_questions = @questions.count
@questions.each do |question_name, question|
@name = question_name
puts pastel.bold(_('[Q %{current_number}/%{questions_total}]') % { current_number: i, questions_total: num_questions })
puts pastel.bold(question[:question])
puts question[:help]
@prompt.print pastel.bold(_('[Q %{current_number}/%{questions_total}]') % { current_number: i, questions_total: num_questions })
@prompt.print pastel.bold(question[:question])
@prompt.print question[:help]
ask(_('-->')) do |q|
q.required(question.fetch(:required, false))

Expand All @@ -40,7 +40,7 @@ def run
q.default(question[:default]) if question.key?(:default)
end
i += 1
puts ''
@prompt.print ''
end
@answers
rescue TTY::Prompt::Reader::InputInterrupt
Expand Down
44 changes: 33 additions & 11 deletions lib/pdk/generators/module.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def self.invoke(opts = {})

prepare_module_directory(temp_target_dir)

template_url = opts.fetch(:'template-url', DEFAULT_TEMPLATE)
template_url = opts.fetch(:'template-url', PDK.answers['template-url'] || DEFAULT_TEMPLATE)

PDK::Module::TemplateDir.new(template_url) do |templates|
templates.render do |file_path, file_content|
Expand All @@ -48,28 +48,42 @@ def self.invoke(opts = {})
end
end

PDK.answers.update!('template-url' => template_url)

FileUtils.mv(temp_target_dir, target_dir)
end

def self.prepare_metadata(opts)
username = Etc.getlogin.gsub(%r{[^0-9a-z]}i, '')
username = 'username' if username == ''
if Etc.getlogin != username
PDK.logger.warn(_('Your username is not a valid Forge username, proceeding with the username %{username}' % { username: username }))
def self.username_from_login
login = Etc.getlogin
login_clean = login.gsub(%r{[^0-9a-z]}i, '')
login_clean = 'username' if login_clean.empty?

if login_clean != login
PDK.logger.warn _('You username is not a valid Forge username, proceeding with the username %{username}') % {
username: login_clean,
}
end

login_clean
end

def self.prepare_metadata(opts)
username = PDK.answers['forge-username'] || username_from_login

defaults = {
'name' => "#{username}-#{opts[:name]}",
'version' => '0.1.0',
'dependencies' => [
{ 'name' => 'puppetlabs-stdlib', 'version_requirement' => '>= 4.13.1 < 5.0.0' },
],
}
defaults['author'] = PDK.answers['author'] unless PDK.answers['author'].nil?
defaults['license'] = PDK.answers['license'] unless PDK.answers['license'].nil?
defaults['license'] = opts[:license] if opts.key? :license

metadata = PDK::Module::Metadata.new(defaults)

module_interview(metadata, opts) unless opts[:'skip-interview'] # @todo Build way to get info by answers file
module_interview(metadata, opts) unless opts[:'skip-interview']

metadata.update!('pdk-version' => PDK::Util::Version.version_string)

Expand Down Expand Up @@ -98,7 +112,7 @@ def self.module_interview(metadata, opts = {})
required: true,
validate_pattern: %r{\A[a-z0-9]+\Z}i,
validate_message: _('Forge usernames can only contain lowercase letters and numbers'),
default: metadata.data['author'],
default: PDK.answers['forge-username'] || metadata.data['author'],
},
{
name: 'version',
Expand Down Expand Up @@ -167,11 +181,13 @@ def self.module_interview(metadata, opts = {})
answers = interview.run

if answers.nil?
puts _('Interview cancelled, aborting...')
PDK.logger.info _('Interview cancelled, not generating the module.')
exit 0
end

forge_username = answers['name']
answers['name'] = "#{answers['name']}-#{opts[:name]}"
answers['license'] = opts[:license] if opts.key?(:license)
metadata.update!(answers)

puts '-' * 40
Expand All @@ -181,10 +197,16 @@ def self.module_interview(metadata, opts = {})
puts '-' * 40
puts

unless prompt.yes?(_('About to generate this module; continue?')) # rubocop:disable Style/GuardClause
puts _('Aborting...')
unless prompt.yes?(_('About to generate this module; continue?'))
PDK.logger.info _('Module not generated.')
exit 0
end

PDK.answers.update!(
'forge-username' => forge_username,
'author' => answers['author'],
'license' => answers['license'],
)
end
end
end
Expand Down
9 changes: 8 additions & 1 deletion spec/acceptance/support/in_a_new_module.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

shared_context 'in a new module' do |name|
before(:all) do
output, status = Open3.capture2e('pdk', 'new', 'module', name, '--skip-interview', '--template-url', "file://#{RSpec.configuration.template_dir}")
argv = [
'pdk', 'new', 'module', name,
'--skip-interview',
'--template-url', "file:///#{RSpec.configuration.template_dir}",
'--answer-file', File.join(Dir.pwd, "#{name}_answers.json")
]
output, status = Open3.capture2e(*argv)

raise "Failed to create test module:\n#{output}" unless status.success?

Expand All @@ -12,5 +18,6 @@
after(:all) do
Dir.chdir('..')
FileUtils.rm_rf(name)
FileUtils.rm("#{name}_answers.json")
end
end
Loading

0 comments on commit 0621158

Please sign in to comment.