lib/origen/code_generators/actions.rb in origen-0.44.0 vs lib/origen/code_generators/actions.rb in origen-0.50.0

- old
+ new

@@ -1,42 +1,108 @@ require 'open-uri' -require 'rbconfig' +require 'set' module Origen module CodeGenerators + # Common helpers available to all Origen code generators. + # Some of these have been copied from Rails and don't make a lot of sense in an Origen context, + # however they are being kept around for now as they serve as good examples of how to write + # generator helpers. module Actions def initialize(*args) # :nodoc: if args.last.is_a?(Hash) @config = args.last.delete(:config) || {} end + @required_acronyms = Set.new super @in_group = nil end def config @config end + def underscored_app_namespace + Origen.app.namespace.to_s.underscore + end + + # Equivalent to calling name.camelcase, but this will identify the need to register any acronyms + # necessary to ensure the camelcased name can be translated back to the original name by the + # underscore method. + # The required acronyms will be saved to an instance variable, @required_acronyms, and calling + # the add_acronyms will add the code to register them to the current application. + def camelcase(name) + name = name.to_s + name.split('_').each do |n| + # Numbers won't be recognized as a split point when going back to underscore, so need to + # register this field beginning with a number as an acronym + @required_acronyms << n if n =~ /^\d/ + end + name.camelcase + end + + def add_acronyms + unless @required_acronyms.empty? + top_level_file = File.join('app', 'lib', "#{underscored_app_namespace}.rb") + if File.exist?(top_level_file) + require_origen = "require 'origen'\n" + prepend_to_file top_level_file, require_origen + comment = "# The following acronyms are required to ensure that auto-loading works\n# properly with some of this application's class names\n" + insert_into_file top_level_file, comment, after: require_origen + @required_acronyms.each do |acronym| + insert_into_file top_level_file, "Origen.register_acronym '#{acronym}'\n", after: comment + end + end + end + end + + # Adds an autoload statement for the given resource name into +app/lib/my_app_name.rb+ + # + # An array of namespaces can optionally be supplied in the arguments. The name and namespaces + # should all be lower cased and underscored. + # + # add_autoload "my_model", namespaces: ["my_namespace", "my_other_namespace"] + def add_autoload(name, options = {}) + namespaces = Array(options[:namespaces]) + # Remove the app namespace if present, we will add the autoload inside the top-level module block + namespaces.shift if namespaces.first == app_namespace + top_level_file = File.join('app', 'lib', "#{underscored_app_namespace}.rb") + if namespaces.empty? + line = " autoload :#{camelcase(name)}, '#{underscored_app_namespace}/#{name}'\n" + insert_into_file top_level_file, line, after: /module #{Origen.app.namespace}\n/ + else + contents = File.read(top_level_file) + regex = "module #{Origen.app.namespace}\s*(#.*)?\n" + indent = '' + namespaces.each do |namespace| + indent += ' ' + new_regex = regex + "(\n|.)*^\s*module #{camelcase(namespace)}\s*(#.*)?\n" + unless contents =~ Regexp.new(new_regex) + lines = "#{indent}module #{camelcase(namespace)}\n" + lines << "#{indent}end\n" + insert_into_file top_level_file, lines, after: Regexp.new(regex), force: true + end + regex = new_regex + end + line = "#{indent} autoload :#{camelcase(name)}, '#{underscored_app_namespace}/#{namespaces.join('/')}/#{name}'\n" + insert_into_file top_level_file, line, after: Regexp.new(regex) + end + end + # Removes (comments out) the specified configuration setting from +config/application.rb+ # # comment_config :semantically_version - def comment_config(*args) - options = args.extract_options! - name = args.first.to_s - + def comment_config(name, options = {}) # Set the message to be shown in logs log :comment, name file = File.join(Origen.root, 'config', 'application.rb') comment_lines(file, /^\s*config.#{name}\s*=.*\n/) end # Adds an entry into +config/application.rb+ - def add_config(*args) - options = args.extract_options! - name, value = args - + def add_config(name, value, options = {}) # Set the message to be shown in logs message = name.to_s if value ||= options.delete(:value) message << " (#{value})" end @@ -51,14 +117,11 @@ # Adds an entry into +Gemfile+ for the supplied gem. # # gem "rspec", group: :test # gem "technoweenie-restful-authentication", lib: "restful-authentication", source: "http://gems.github.com/" # gem "rails", "3.0", git: "git://github.com/rails/rails" - def gem(*args) - options = args.extract_options! - name, version = args - + def gem(name, version, options = {}) # Set the message to be shown in logs. Uses the git repo if one is given, # otherwise use name (version). parts, message = [quote(name)], name if version ||= options.delete(:version) parts << quote(version) @@ -127,11 +190,11 @@ sentinel = /class [a-z_:]+ < Rails::Application/i env_file_sentinel = /Rails\.application\.configure do/ data = yield if !data && block_given? in_root do - if options[:env].nil? + if options[:env].nil?.map(&:camelcase).join('::') inject_into_file 'config/application.rb', "\n #{data}", after: sentinel, verbose: false else Array(options[:env]).each do |env| inject_into_file "config/environments/#{env}.rb", "\n #{data}", after: env_file_sentinel, verbose: false end @@ -198,29 +261,179 @@ argument = args.flat_map(&:to_s).join(' ') in_root { run_ruby_script("bin/rails generate #{what} #{argument}", verbose: false) } end - # Runs the supplied rake task - # - # rake("db:migrate") - # rake("db:migrate", env: "production") - # rake("gems:install", sudo: true) - def rake(command, options = {}) - log :rake, command - env = options[:env] || ENV['RAILS_ENV'] || 'development' - sudo = options[:sudo] && RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ ? 'sudo ' : '' - in_root { run("#{sudo}#{extify(:rake)} #{command} RAILS_ENV=#{env}", verbose: false) } - end - # Reads the given file at the source root and prints it in the console. # # readme "README" def readme(path) log File.read(find_in_source_paths(path)) end + # Should probably move to its own file, these are general helpers rather than actions + module Helpers + # Returns the depth of the given file, where depth is the number of modules and classes it contains + def internal_depth(file) + depth = 0 + File.readlines(file).each do |line| + if line =~ /^\s*(end|def)/ + return depth + elsif line =~ /^\s*(module|class)/ + depth += 1 + end + end + end + + # Only executes the given block if the given file does not already define the given method, where the + # block would normally go on to insert the method. + # + # See the ensure_define_sub_blocks method in the sub_blocks.rb generator for a usage example. + def unless_has_method(filepath, name) + unless File.read(filepath) =~ /^\s*def #{name}(\(|\s|\n)/ + yield + end + end + + # Executes the given block unless the given string is lower cased and underscored and doesn't start + # with a number of contain any special characters + def unless_valid_underscored_identifier(str) + if str =~ /[^0-9a-z_]/ || str =~ /^[0-9]/ + yield + end + end + + def validate_resource_path(name) + name.split('/').each do |n| + unless_valid_underscored_identifier(n) do + Origen.log.error "All parts of a resource name must be lower-cased, underscored and start with letter, '#{n}' is invalid" + exit 1 + end + end + name + end + alias_method :validate_resource_name, :validate_resource_path + + # Converts a path to a resource identifier, by performing the following operations on the given path: + # 1) Convert any absolute paths to relative + # 2) Removes any leading blocks/, lib/ or application namespaces + # 3) Remove any derivatives directories from the path + # 3) Removes any trailing .rb + # + # Examples: + # + # /my/code/my_app/app/blocks/dut/derivatives/falcon => dut/falcon + # app/lib/my_app/eagle.rb => eagle + def resource_path(path) + path = Pathname.new(path).expand_path.relative_path_from(Pathname.pwd).to_s + path = path.sub('.rb', '') + path = path.split('/') + from_block_dir_path = false + path.shift if path.first == 'app' + path.shift if path.first == 'lib' + if path.first == 'blocks' + path.shift + from_block_dir_path = true + end + path.shift if path.first == underscored_app_namespace + if path.include?('derivatives') + path.delete('derivatives') + from_block_dir_path = true + end + if from_block_dir_path + path.delete('sub_blocks') + path.pop if path.last == 'model' + if path.last == 'controller' + path.pop + path << "#{path.pop}_controller" + end + end + path.join('/') + end + + # Returns a Pathname to the blocks directory that should contain the given class name. No checking is + # done of the name and it is assumed that it is a valid class name including the application namespace. + def class_name_to_blocks_dir(name) + name = name.split('::') + name.shift # Drop the application name + dir = Origen.root.join('app', 'blocks') + name.each_with_index do |n, i| + if i == 0 + dir = dir.join(n.underscore) + else + dir = dir.join('derivatives', n.underscore) + end + end + dir + end + + # Returns a Pathname to the lib directory file that should contain the given class name. No checking is + # done of the name and it is assumed that it is a valid class name including the application namespace. + def class_name_to_lib_file(name) + name = name.split('::') + dir = Origen.root.join('app', 'lib') + name.each_with_index do |n, i| + dir = dir.join(i == name.size - 1 ? "#{n.underscore}.rb" : n.underscore) + end + dir + end + + def resource_path_to_blocks_dir(path) + name = resource_path(path).split('/') # Ensure this is clean, don't care about performance here + dir = Origen.root.join('app', 'blocks') + name.each_with_index do |n, i| + if i == 0 + dir = dir.join(n.underscore) + else + if dir.join('sub_blocks', n.underscore).exist? + dir = dir.join('sub_blocks', n.underscore) + else + dir = dir.join('derivatives', n.underscore) + end + end + end + dir + end + + def resource_path_to_lib_file(path) + name = resource_path(path).split('/') # Ensure this is clean, don't care about performance here + dir = Origen.root.join('app', 'lib', underscored_app_namespace) + name.each_with_index do |n, i| + dir = dir.join(i == name.size - 1 ? "#{n.underscore}.rb" : n.underscore) + end + dir + end + + def resource_path_to_class(path) + name = resource_path(path).split('/') # Ensure this is clean, don't care about performance here + name.unshift(underscored_app_namespace) + name.map { |n| camelcase(n) }.join('::') + end + + # Adds :class and :module identifiers to an array of namespaces + # + # ["my_app", "models", "bist"] => [[:module, "my_app"], [:module, "models"], [:class, "bist"]] + # + def add_type_to_namespaces(namespaces) + identifier = nil + namespaces.map do |namespace| + if identifier + identifier += "::#{camelcase(namespace)}" + else + identifier = camelcase(namespace) + end + begin + const = identifier.constantize + [const.is_a?(Class) ? :class : :module, namespace] + rescue NameError + [:module, namespace] + end + end + end + end + include Helpers + protected # Define log for backwards compatibility. If just one argument is sent, # invoke say, otherwise invoke say_status. Differently from say and # similarly to say_status, this method respects the quiet? option given. @@ -231,20 +444,17 @@ args << (behavior == :invoke ? :green : :red) say_status(*args) end end - # Add an extension to the given name based on the platform. - def extify(name) - if RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ - "#{name}.bat" - else - name + def in_root + Dir.chdir(Origen.root) do + yield end end - # Surround string with single quotes if there is no quotes. - # Otherwise fall back to double quotes + # Surround string with single quotes if there are no quotes, + # otherwise fall back to double quotes def quote(value) return value.inspect unless value.is_a? String if value.include?("'") value.inspect