Class: Linecook::Commands::CompileHelper
- Inherits:
-
Linecook::Command
- Object
- Linecook::Command
- Linecook::Commands::CompileHelper
- Includes:
- Utils
- Defined in:
- lib/linecook/commands/compile_helper.rb
Overview
:startdoc::desc compile helper modules
Compiles a 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 output directory in a file corresponding to const_name (which can also be a constant path). Input directories may be specified to automatically search for source files based on constant path. These are all are equivalent and produce the Const::Name module in ‘lib/const/name.rb’:
$ linecook compile-helper Const::Name helpers/const/name/*
$ linecook compile-helper const/name helpers/const/name/*
$ linecook compile-helper const/name -i helpers
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 header separated from the body by a double-dash, like this:
[echo.erb]
Echo arguments out to the target.
(*args)
--
echo <%= args.join(' ') %>
Which produces something like:
# Echo arguments out to the target.
def echo(*args)
eval ERB.new("echo <%= args.join(' ') %>").src
end
A second ‘capture’ method is also generated to return the result without writing it to the target. The latter method is prefixed by an underscore like this:
# Return the output of echo, without writing to the target
def _echo(*args)
...
end
Special characters can be added to a method name by using a -extension to the file name. For example ‘file-check.erb’ defines the ‘file?’ method. These extensions are supported:
extension character
-check ?
-bang !
-eq =
Otherwise the basename of the source file must be a valid method name; invalid names raise an error.
Section Files
Section files are source files that can be used to insert 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, like other source files, processed according to their extname:
extname translation
.rb file defines ruby (directly inserted)
.rdoc file defines RDoc (lines commented out, then inserted)
Note that section files prevent methods beginning with an underscore; this is intentional and prevents collisions with capture methods.
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 <% end %> <%= sections['foot'] %> DOC
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.
-
#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
-
#search_sources(const_path) ⇒ Object
Returns source files for a given constant path, which are all files under the ‘search_dir/const_path’ folder.
-
#section_content(content, extname) ⇒ Object
:nodoc:.
Methods included from Utils
camelize, constantize, deep_merge, deep_merge?, underscore
Methods inherited from Linecook::Command
#call, help, #initialize, parse, signature
Constructor Details
This class inherits a constructor from Linecook::Command
Instance Method Details
#build(const_name, sources) ⇒ Object
Returns the code for a const_name module as defined by the source files.
288 289 290 291 292 293 294 295 296 297 |
# File 'lib/linecook/commands/compile_helper.rb', line 288 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.
148 149 150 |
# File 'lib/linecook/commands/compile_helper.rb', line 148 def const_name?(const_name) # :nodoc: const_name =~ /\A(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*\z/ end |
#load_definition(path) ⇒ Object
helper to load and parse a definition file
197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/linecook/commands/compile_helper.rb', line 197 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("invalid source file: #{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.
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/linecook/commands/compile_helper.rb', line 164 def load_sections(paths) # :nodoc: sections = {} paths.each do |path| begin basename = File.basename(path) extname = File.extname(path) key = basename[1, basename.length - extname.length - 1] sections[key] = section_content(File.read(path), extname) rescue CommandError err = CommandError.new("invalid source file: #{path.inspect} (#{$!.})") err.set_backtrace($!.backtrace) raise err end 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).
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 |
# File 'lib/linecook/commands/compile_helper.rb', line 240 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 # remove encoding comment in 1.9 because it is not needed code = code.sub(/\A#coding:.*?\n/, '') "#{source}\n#{code}".gsub(/^(\s*)/) do |m| indent = 2 + $1.length - ($1.length % 2) ' ' * indent end when '.rb' body.rstrip else raise CommandError.new("unsupported 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.
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/linecook/commands/compile_helper.rb', line 267 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.empty? ? 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
211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/linecook/commands/compile_helper.rb', line 211 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
226 227 228 229 230 231 232 233 234 |
# File 'lib/linecook/commands/compile_helper.rb', line 226 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[A-Za-z]\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
154 155 156 157 158 159 160 |
# File 'lib/linecook/commands/compile_helper.rb', line 154 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
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/linecook/commands/compile_helper.rb', line 104 def process(const_name, *sources) const_path = underscore(const_name) const_name = camelize(const_name) unless const_name?(const_name) raise CommandError, "invalid constant name: #{const_name.inspect}" end sources = sources | search_sources(const_path) target = File.(File.join(output_dir, "#{const_path}.rb")) if sources.empty? raise CommandError, "no sources specified" 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 |
#search_sources(const_path) ⇒ Object
Returns source files for a given constant path, which are all files under the ‘search_dir/const_path’ folder.
136 137 138 139 140 141 142 143 144 145 |
# File 'lib/linecook/commands/compile_helper.rb', line 136 def search_sources(const_path) sources = [] search_dirs.each do |search_dir| pattern = File.join(search_dir, const_path, '*') sources.concat Dir.glob(pattern) end sources.select {|path| File.file?(path) } end |
#section_content(content, extname) ⇒ Object
:nodoc:
183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/linecook/commands/compile_helper.rb', line 183 def section_content(content, extname) # :nodoc: case extname when '.rdoc' content.gsub(/^/, '# ') when '.rb' content else raise CommandError.new("unsupported section format #{extname.inspect}") end end |