Class: Linecook::Commands::Helper
- Includes:
- Utils
- Defined in:
- lib/linecook/commands/helper.rb
Overview
:startdoc::desc generates a helper module
Generates the specified helper module from a set of source files. Each source file becomes a method in the module, named after the source file itself.
The helper module will be generated under the lib directory in a file corresponding to const_name (which can also be a constant path). By default, all files under the corresponding helpers directory will be used as sources. For example these are equivalent and produce the Const::Name module in ‘lib/const/name.rb’:
% linecook helper Const::Name
% linecook helper const/name
% linecook helper const/name helpers/const/name/*
Source Files
The contents of the source file are translated into code according to the source file extname.
extname translation
.rb file defines method body
.erb file defines an ERB template (compiled to ruby code)
Source files can specify documenation and a method signature using a standard header separated from the body by a double-dash. For example this:
[echo.erb]
Echo arguments out to the target.
(*args)
--
echo <%= args.join(' ') %>
Is translated into something like:
# Echo arguments out to the target.
def echo(*args)
eval ERB.new("echo <%= args.join(' ') %>").src
end
A second method is also generated to return the result without writing it to the target. The latter method is prefixed by and underscore like:
# Return the output of echo, without writing to the target
def _echo(*args)
...
end
Check and bang methods can be specified by adding -check and -bang to the end of the file name. These extensions are stripped off like:
[file-check.erb] # => def file? ...
[make-bang.rb] # => def make! ...
Otherwise the basename of the source file must be a word; non-word basenames raise an error.
Section Files
Special section files can be used to define non-standard code in the following places:
[:header]
module Const
[:doc]
module Name
[:head]
...
[:foot]
end
end
[:footer]
Section files are defined by prepending ‘-’ to the file basename (like path/to/-header.rb) and are not processed like other source files; instead the contents are directly transcribed into the target file.
Constant Summary collapse
- MODULE_TEMPLATE_LINE =
:stopdoc:
__LINE__ + 2
- MODULE_TEMPLATE =
ERB.new(<<-DOC, nil, '<>').src # Generated by Linecook <%= sections['header'] %> <%= module_nest(const_name, body, sections['doc']) %> <%= sections['footer'] %> DOC
- DEFINITION_TEMPLATE_LINE =
__LINE__ + 2
- DEFINITION_TEMPLATE =
ERB.new(<<-DOC, nil, '<>').src <%= sections['head'] %> <% definitions.each do |desc, method_name, signature, method_body| %> <% desc.split("\n").each do |line| %> # <%= line %><% end %> def <%= method_name %><%= signature %> <%= method_body %> chain_proxy end def _<%= method_name %>(*args, &block) # :nodoc: str = capture_str { <%= method_name %>(*args, &block) } str.strip! str end <% end %> <%= sections['foot'] %> DOC
Constants inherited from Command
Instance Attribute Summary
Attributes inherited from Command
Instance Method Summary collapse
-
#build(const_name, sources) ⇒ Object
Returns the code for a const_name module as defined by the source files.
-
#const_name?(const_name) ⇒ Boolean
returns true if const_name is a valid constant name.
-
#default_sources(const_path) ⇒ Object
Returns the default source files for a given constant path, which are all files under the ‘project_dir/helpers/const_path’ folder.
-
#load_definition(path) ⇒ Object
helper to load and parse a definition file.
-
#load_sections(paths) ⇒ Object
helper to load each section path into a sections hash; removes the leading - from the path basename to determine the section key.
-
#method_body(body, extname) ⇒ Object
helper to reformat a definition body according to a given extname.
-
#module_nest(const_name, body, inner_doc = nil) ⇒ Object
helper to nest a module body within a const_name.
-
#parse_definition(str) ⇒ Object
helper to reformat special basenames (in particular -check and -bang) to their corresponding method_name.
-
#parse_method_name(basename) ⇒ Object
helper to reformat special basenames (in particular -check and -bang) to their corresponding method_name.
-
#partition(sources) ⇒ Object
helper to partition an array of source files into section and defintion files.
- #process(const_name, *sources) ⇒ Object
Methods included from Utils
arrayify, camelize, constantize, deep_merge, deep_merge?, hashify, load_config, underscore
Methods inherited from Command
#call, inherited, #initialize, #log, registry, #sh, #sh!
Constructor Details
This class inherits a constructor from Linecook::Commands::Command
Instance Method Details
#build(const_name, sources) ⇒ Object
Returns the code for a const_name module as defined by the source files.
252 253 254 255 256 257 258 259 260 261 |
# File 'lib/linecook/commands/helper.rb', line 252 def build(const_name, sources) section_paths, definition_paths = partition(sources) sections = load_sections(section_paths) definitions = definition_paths.collect {|path| load_definition(path) } body = eval DEFINITION_TEMPLATE, binding, __FILE__, DEFINITION_TEMPLATE_LINE code = eval MODULE_TEMPLATE, binding, __FILE__, MODULE_TEMPLATE_LINE code end |
#const_name?(const_name) ⇒ Boolean
returns true if const_name is a valid constant name.
134 135 136 |
# File 'lib/linecook/commands/helper.rb', line 134 def const_name?(const_name) # :nodoc: const_name =~ /\A(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*\z/ end |
#default_sources(const_path) ⇒ Object
Returns the default source files for a given constant path, which are all files under the ‘project_dir/helpers/const_path’ folder.
127 128 129 130 131 |
# File 'lib/linecook/commands/helper.rb', line 127 def default_sources(const_path) pattern = File.join(project_dir, 'helpers', const_path, '*') sources = Dir.glob(pattern) sources.select {|path| File.file?(path) } end |
#load_definition(path) ⇒ Object
helper to load and parse a definition file
164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/linecook/commands/helper.rb', line 164 def load_definition(path) # :nodoc: extname = File.extname(path) name = File.basename(path).chomp(extname) desc, signature, body = parse_definition(File.read(path)) [desc, parse_method_name(name), signature, method_body(body, extname)] rescue CommandError err = CommandError.new("#{$!.} (#{path.inspect})") err.set_backtrace($!.backtrace) raise err end |
#load_sections(paths) ⇒ Object
helper to load each section path into a sections hash; removes the leading - from the path basename to determine the section key.
150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/linecook/commands/helper.rb', line 150 def load_sections(paths) # :nodoc: sections = {} paths.each do |path| basename = File.basename(path) extname = File.extname(path) key = basename[1, basename.length - extname.length - 1] sections[key] = File.read(path) end sections end |
#method_body(body, extname) ⇒ Object
helper to reformat a definition body according to a given extname. rb content is rstripped to improve formatting. erb content is compiled and the source is placed as a comment before it (to improve debugability).
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/linecook/commands/helper.rb', line 207 def method_body(body, extname) # :nodoc: case extname when '.erb' source = "# #{body.gsub(/\n/, "\n# ")}" compiler = ERB::Compiler.new('<>') compiler.put_cmd = "write" compiler.insert_cmd = "write" code = [compiler.compile(body)].flatten.first "#{source}\n#{code}".gsub(/^(\s*)/) do |m| indent = 2 + $1.length - ($1.length % 2) ' ' * indent end when '.rb' body.rstrip else raise CommandError.new("invalid definition format: #{extname.inspect}") end end |
#module_nest(const_name, body, inner_doc = nil) ⇒ Object
helper to nest a module body within a const_name. documentation can be provided for the innermost constant.
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/linecook/commands/helper.rb', line 231 def module_nest(const_name, body, inner_doc=nil) # :nodoc: body = body.strip.split("\n") const_name.split(/::/).reverse_each do |name| body.collect! {|line| " #{line}" } body.unshift "module #{name}" body.push "end" # prepend the inner doc to the innermost const if inner_doc body = inner_doc.strip.split("\n") + body inner_doc = nil end end body.join("\n") end |
#parse_definition(str) ⇒ Object
helper to reformat special basenames (in particular -check and -bang) to their corresponding method_name
178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/linecook/commands/helper.rb', line 178 def parse_definition(str) # :nodoc: head, body = str.split(/^--.*\n/, 2) head, body = '', head if body.nil? found_signature = false signature, desc = head.split("\n").partition do |line| found_signature = true if line =~ /^\s*\(.*?\)/ found_signature end [desc.join("\n"), found_signature ? signature.join("\n") : '()', body.to_s] end |
#parse_method_name(basename) ⇒ Object
helper to reformat special basenames (in particular -check and -bang) to their corresponding method_name
193 194 195 196 197 198 199 200 201 |
# File 'lib/linecook/commands/helper.rb', line 193 def parse_method_name(basename) # :nodoc: case basename when /-check\z/ then basename.sub(/-check$/, '?') when /-bang\z/ then basename.sub(/-bang$/, '!') when /-eq\z/ then basename.sub(/-eq$/, '=') when /\A\w+\z/ then basename else raise CommandError.new("invalid method name: #{basename.inspect}") end end |
#partition(sources) ⇒ Object
helper to partition an array of source files into section and defintion files
140 141 142 143 144 145 146 |
# File 'lib/linecook/commands/helper.rb', line 140 def partition(sources) # :nodoc: sources.partition do |path| basename = File.basename(path) extname = File.extname(path) basename[0] == ?- && basename.chomp(extname) != '-' end end |
#process(const_name, *sources) ⇒ Object
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/linecook/commands/helper.rb', line 95 def process(const_name, *sources) const_path = underscore(const_name) const_name = camelize(const_path) unless const_name?(const_name) raise "invalid constant name: #{const_name.inspect}" end sources = default_sources(const_path) if sources.empty? target = File.(File.join('lib', "#{const_path}.rb"), project_dir) if sources.empty? raise CommandError, "no sources specified (and none found under 'helpers/#{const_path}')" end if force || !FileUtils.uptodate?(target, sources) content = build(const_name, sources) target_dir = File.dirname(target) unless File.exists?(target_dir) FileUtils.mkdir_p(target_dir) end File.open(target, 'w') {|io| io << content } $stdout.puts target unless quiet end target end |