# angular_init (short-name: ngi) # Copyright 2015 Joshua Beam # github.com/joshbeam/angular_init # MIT License # TODO: Method to clean out unused custom template files require_relative 'utils/utils' require 'fileutils' require 'yaml' # Run the user through an interactive # session of configuring ngi class Configure attr_accessor :file, :location JSer = Utils::JSer Utils::CurrentDir.dir = File.dirname(__FILE__) # STDIN is separated into a class so that # it can be extracted and tested class AcceptInput def self.str(type) case type when :stripped $stdin.gets.strip end end end # Here, we implement the virtual class "Utils::AskLoop" # (see src/ruby/utils/utils.rb) # This implementation just creates a loop that asks # for user input class AskLoop < Utils::AskLoop def self.ask(args) puts "\nChoose from: #{args[:valid]}" answer = AcceptInput.str(:stripped) loop do break if args[:check].include?(answer) puts "Choose from: #{args[:valid]}\n(or press ctrl+c to exit)" answer = AcceptInput.str(:stripped) end answer end end # An extraction of the template file/directory for # the generator... This way it can be separated, redefined, # and tested. class TemplateDir attr_reader :d def initialize(component, language, template) @d = "#{Utils::CurrentDir.dir}/templates/" @d << "#{component['type']}/#{language}" @d << "/user/#{template}" end def read f = File.open(@d) content = f.read f.close content end end # Holds all the configure functions for the properties # that are configurable class Configurable def self.language(config) v = JSer.new(config.lang_types).to_str type = AskLoop.ask(check: config.lang_types, valid: v) curr_lang = config.config['language'][type] lang_opts = config.languages[type].reject { |l| l if curr_lang == l } v = JSer.new(lang_opts).to_str language = AskLoop.ask(check: lang_opts, valid: v) answer = config.config['language'] answer[type] = language answer end def self.create_template_file(args) component = args[:component] language = args[:language] template = args[:template] template_dir = TemplateDir.new(component, language, template).d destination = File.dirname(template_dir) # Create the directory "user" unless it already exists FileUtils.mkdir_p(destination) unless File.directory?(destination) # The actual custom file custom_file = template # Copy the custom file into the new or existing "user" directory if File.exist? custom_file # TODO: Add a 'safety net' by checking if the user # has already added the custom template before # so that they don't overwrite it FileUtils.cp(custom_file, destination) else puts "Cannot find custom template file: '#{custom_file}' Check to make sure the custom template file exists, and that you're in the correct directory of the custom template." exit end end def self.templates(config) v_c = JSer.new(config.components).to_str component = AskLoop.ask(check: config.components, valid: v_c) chosen_component = -> (c) { c['name'] == component } print '[?] Use the following template file: ' file_name = AcceptInput.str(:stripped) print '[?] What language is this template for?' type = config.components_hash.find(&chosen_component)['type'] v_l = JSer.new(config.languages[type]).to_str language = AskLoop.ask(check: config.languages[type], valid: v_l) chosen_language = -> (t) { t['language'] == language } # Our answer will be an empty array first, # because Tempaltes: might not exist the # config.yml file answer = [] # Set the answer to Templates: from the config.yml # file if it exists answer = config.config['templates'] if config.config.key? 'templates' # Then we want to see if the component already exists in # the config file exists = false existing_component = answer.find(&chosen_component) exists = true if !existing_component == false if file_name != 'default' if exists == true # Remove the current existing component (it's already saved above) answer = answer .reject(&chosen_component) # Remove the existing template object existing_component['templates'] = existing_component['templates'] .reject(&chosen_language) # Put in the updated template object existing_component['templates'] << { 'language' => language, 'template' => file_name } # Push the existing component back into the templates array answer << existing_component elsif exists == false # If it isn't already there, # go ahead and add it to the templates array answer << { 'name' => component, 'type' => type, 'templates' => [ { 'language' => language, 'template' => file_name } ] } end create_template_file( config: config.config, component: answer.last, language: language, template: file_name ) else # If default is chosen as the template, # then delete the template from the templates # array for that component answer .find(&chosen_component)['templates'] .delete_if(&chosen_language) # If the templates array of the component # is empty, then delete the entire component # from the answer answer .delete_if(&chosen_component) if answer .find(&chosen_component)['templates'] .size == 0 end if answer.size == 0 nil else answer end end end # Make Questioner accesible as in: Configure::Questioner.run() # "Questioner" simply starts an interactive prompt to guide # the user in configuring src/config/agular_init.config.json, # which is a JSON file that holds all the global configurable # options (like language to use, templates, etc.) class Questioner attr_accessor :file attr_reader :configurable, :languages, :config, :components, :components_hash, :lang_types def initialize(args) @languages = args[:languages] language_types = @languages.select do |type, languages| type if languages.size > 1 end @lang_types = language_types.collect { |type, _| type }.flatten @components_hash = args[:components_hash] @components = args[:components] @config = args[:config] @configurable = args[:configurable] yield(self) if block_given? end def choose_configurable_property puts "Current settings\n================" @configurable.each_with_index do |p, i| json_string = 'Currently using default settings' json_string = JSer.new(@config[p]).to_str if @config.key? p puts "#{i + 1}) #{p.capitalize}: #{json_string}" end valid = JSer.new(@configurable).to_str # return AskLoop.ask(check: @configurable, valid: valid) end # This method delegates to the appropriate # Configurable# based on the property # that the user has chosen to configure # Returns: a Hash of the object, based on the # from_json config object from config/angular_init.config.json def configure_property(property) case property when 'language' return Configurable.language(self) when 'templates' return Configurable.templates(self) end end def self.run(file) questioner = Questioner.new(file) do |q| # First, the user chooses a property # to configure (from a list of available # properties that *can* be configured) property = q.choose_configurable_property # #configure_property spits out a hash # as the result result = q.configure_property(property) # The hash that was spit out as the # result is "merged" into the original # Hash from_json object that came from # config/angular_init.config.json # and is inside of this instance of Questioner q.config[property] = result # delete any properties that are nil q.config.delete_if { |_, v| v.nil? } # This just tells the user that we were # successful result_string_hash = JSer.new(result).to_str rescue 'null' puts "#{property.capitalize} set to: #{result_string_hash}" end # Returns the file so that it can be used # (For example, Configure might write this # new hash as a JSON file to # config/angular_init.config.json) questioner.config end end # The only thing we do here is load the JSON config file basically # as just a string in JSON format. # It will be converted to a Ruby hash in from_json below def initialize(location = nil) unless location.nil? @location = location f = File.open(@location, 'r') @file = f.read f.close end yield(self) if block_given? end # Convert the file to a Ruby hash # def from_json # JSON.parse(@file) # end # Convert the file to a Ruby hash # Usage: Configure.new('file/path').to_ruby(from: 'yaml') def to_ruby(args) case args[:from] when 'json' JSON.parse(@file) when 'yaml' YAML.load(@file) end end # Convert the file from a Ruby hash # into whatever language is specified # Usage: .from_ruby(to: 'yaml') def from_ruby(args) case args[:to] when 'json' JSON.pretty_generate(@file) when 'yaml' @file.to_yaml end end # Here we actually write the new JSON config file # to config/angular_init.config.json def write(args) File.open(args[:destination], 'w') do |f| f.write(args[:file]) f.close end end # All this method does is handle retrieving # the file from config/angular_init.config.json # so that it can pass it off to Questioner, # which can in turn change the Hash and pass it # *back* to Configure, which can then choose # to actually write the file in JSON format # to config/angular_init.config.json def self.run(args) Configure.new do |c| c.file = Configure::Questioner.run(args) if args[:write] == true c.write( destination: args[:destination], file: c.from_ruby(to: args[:to]) ) end end end end