#! /usr/bin/env ruby # # ActiveFacts: Read a model (CQL, ORM, etc), run a compositor, then a generator # # Copyright (c) 2009 Clifford Heath. Read the LICENSE file. # ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) $:.unshift File.dirname(File.expand_path(__FILE__))+"/../lib" require 'pathname' require 'activefacts/loadable' require 'activefacts/metamodel' require 'activefacts/compositions' require 'activefacts/generator' class SchemaCompositor EXTENSIONS = ['fiml', 'fidl', 'fiql', 'cql'] attr_reader :options attr_reader :compositors attr_reader :generators # Parse options into a hash, and values for each option into a hash def initialize argv @argv = argv.dup @options = {} while @argv[0] =~ /^-/ option, value = @argv.shift.split(/[=:]/, 2) csv = (value =~ /,/ ? value.split(',') : Array(value)) modes = csv.inject({}){|h,s| k, v = s.split(/=/, 2); h[k] = v || true; h } @options[option.sub(/^-*/,'')] = modes end end # Load and enumerate all available modules in this path def enumerate_available path trace :loading, "Enumerating under #{path.inspect}" do Loadable.new(path). enumerate. map do |filename| begin require(pathname = path+"/"+filename) trace :loading, "Loaded #{pathname}" filename rescue LoadError => e trace :loading, "Can't load #{pathname}: #{e.class}: #{e.message} #{e.backtrace[0]}" nil rescue Exception => e $stderr.puts "Can't load #{pathname}: #{e.class}: #{e.message} #{e.backtrace[0]}" nil end end.compact end end def arrange_actions # Arrange the requested compositors and generators: @compositors = [] @generators = [] @options.clone.each do |option, modes| # Flip option and first mode if option is source or target if option == 'source' || option == 'target' compositor, flag = modes.shift modes[option] = true option = compositor end # Is it a compositor? action, helptext = ActiveFacts::Compositions.compositors[option] if action options.delete(option) check_options(action, modes) @compositors << [action, modes, option] next end # Is it a generator? action, helptext = ActiveFacts::Generators.generators[option] if action options.delete(option) check_options(action, modes) @generators << [action, modes, option] next end if option == 'help' # Finish, then help else $stderr.puts "Action --#{option} is not recognised" exit 1 end if modes['help'] puts "Help for #{option} is not yet available" end end end def process_files # Process each input file: @argv.each do |arg| filename, input_options = *arg.split(/=/, 2) # Load the correct file type input method pathname, basename, extension = * /(?:(.*)[\/\\])?(.*)\.([^.]*)$/.match(filename).captures if EXTENSIONS.detect { |e| extension == e } extension = "cql" end input_handler = "activefacts/input/#{extension}" require input_handler input_class = extension.upcase input_klass = ActiveFacts::Input.const_get(input_class.to_sym) raise "Expected #{input_handler} to define #{input_class}" unless input_klass # Read the input file: vocabulary = if input_klass begin input_klass.readfile(filename, *input_options) rescue => e $stderr.puts "#{e.message}" if trace :exception $stderr.puts "\t#{e.backtrace*"\n\t"}" else $stderr.puts "\t#{e.backtrace[0]}" end exit 1 end end exit 0 unless vocabulary vocabulary.finalise unless vocabulary == true constellation = vocabulary.constellation compositions = @compositors.map do |compositor_klass, modes, option| compositor = compositor_klass.new(constellation, basename, modes) compositor.generate [compositor_klass, compositor.composition] end begin # Run each generator @generators.each do |generator_klass, modes| arity, composition_types = generator_klass.compatibility type_list = composition_types ? composition_types.map(&:to_s)*' or ' : nil compatible_compositions = compositions. select{|k, c| composition_types ? (composition_types&k.compatibility).size > 0 : true} if arity == 0 # No composition is required output = generator_klass.new(constellation, compositions.map{|k,c| c}, modes).generate puts output if output elsif arity == 1 # This generator processes each composition in turn if compatible_compositions.size == 0 raise "#{generator_klass.basename} expects a #{type_list} compositor; use --help for a list" end compatible_compositions. each do |k, c| output = generator_klass.new(constellation, c, modes).generate puts output if output end else # This generator either accepts "arity" compositions or any number if arity && compositions.size != arity raise "#{generator_klass.basename} expects #{arity} #{type_list} compositions; use --help for a list" end # Pass all compositions: output = generator_klass.new(constellation, compositions.map{|k,c| c}, modes).generate puts output if output end end rescue => e $stderr.puts "#{e.message}" if trace :exception $stderr.puts "\t#{e.backtrace*"\n\t"}" end end end end def action_name action action.name.sub(/ActiveFacts::[^:]+::/,'').gsub(/::/,'/').downcase end def display_options action, stream = $stdout options = action.options name = action.name.sub(/ActiveFacts::[^:]+::/,'').gsub(/::/,'/').downcase if options.empty? stream.puts "There are no options for --#{action_name action}" else stream.puts "Options for --#{name} (say e.g. --#{action_name action}:option1=value,option2)" options.keys.sort.each do |key| type, description = *options[key] tag = key.to_s + case type when NilClass,'Boolean', TrueClass '' when Numeric ' num' when Pathname ' file' when Array " { #{type.map(&:to_s)*' | '} }" else ' str' end spaces = (s = 24-tag.size) < 2 ? 2 : s stream.puts "\t#{tag}#{' '*spaces}#{description}" end end end # Ensure that the options provided are supported by the action def check_options action, modes if modes['help'] display_options(action) exit end options = action.options unsupported = modes.keys.select{|k| !options.has_key?(k.to_sym)} return if unsupported.empty? $stderr.puts "Action --#{action_name action} does not support #{unsupported.size >1 ? 'these options' : 'this option'}: #{unsupported*', '}" display_options(action, $stderr) exit 1 end end sc = SchemaCompositor.new(ARGV) sc.enumerate_available('activefacts/compositions') sc.enumerate_available('activefacts/generator') inputs = sc.enumerate_available('activefacts/input') sc.arrange_actions if sc.options['help'] || (sc.generators.empty? && sc.compositors.empty?) compositors = (c = ActiveFacts::Compositions.compositors).keys.sort.map{|k| "%-15s %s" % [k, c[k][1]]} generators = (c = ActiveFacts::Generators.generators).keys.sort.map{|k| "%-15s %s" % [k, c[k][1]]} puts "You need to use a compositor to create the right kind of schema, and at least one generator to get output." puts "Use '#{$PROGRAM_NAME} -- -- inputfile.ext'" puts "Available input formats (file extensions):\n\t#{inputs*"\n\t"}\n\n" puts "Available compositors:\n\t#{compositors*"\n\t"}\n\n" puts "Available generators:\n\t#{generators*"\n\t"}\n\n" puts "To get help for a particular action, follow it by =help, e.g. --relational:help" exit end sc.process_files