Class: Linecook::Commands::CompileHelper

Inherits:
Linecook::Command show all
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

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.

Returns:

  • (Boolean)


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} (#{$!.message})")
  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} (#{$!.message})")
      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.expand_path(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