lib/qme/map/map_reduce_builder.rb in quality-measure-engine-0.1.2 vs lib/qme/map/map_reduce_builder.rb in quality-measure-engine-0.2.0

- old
+ new

@@ -1,169 +1,99 @@ +require 'erb' +require 'ostruct' + module QME module MapReduce + + # Builds Map and Reduce functions for a particular measure class Builder - attr_reader :id, :parameters + attr_reader :id, :params - YEAR_IN_SECONDS = 365*24*60*60 + # Utility class used to supply a binding to Erb + class Context < OpenStruct + # Create a new context + # @param [Hash] vars a hash of parameter names (String) and values (Object). Each entry is added as an accessor of the new Context + def initialize(db, vars) + super(vars) + @db = db + end + + # Get a binding that contains all the instance variables + # @return [Binding] + def get_binding + binding + end + + # Inserts any library code into the measure JS. JS library code is loaded from + # three locations: the js directory of the quality-measure-engine project, the + # js sub-directory of the current directory (e.g. measures/js), and the bundles + # collection of the current database (used by the Rails Web application). + def init_js_frameworks + result = '' + result << 'if (typeof(map)=="undefined") {' + result << "\n" + Dir.glob(File.join(File.dirname(__FILE__), '../../../js/*.js')).each do |js_file| + result << File.read(js_file) + result << "\n" + end + Dir.glob(File.join('./js/*.js')).each do |js_file| + result << File.read(js_file) + result << "\n" + end + @db['bundles'].find.each do |bundle| + (bundle['extensions'] || []).each do |ext| + result << ext + result << "\n" + end + end + result << "}\n" + result + end + end - def initialize(measure_def, params) - @measure_def = measure_def + # Create a new Builder + # @param [Hash] measure_def a JSON hash of the measure, field values may contain Erb directives to inject the values of supplied parameters into the map function + # @param [Hash] params a hash of parameter names (String or Symbol) and their values + def initialize(db, measure_def, params) @id = measure_def['id'] - @parameters = {} - measure_def['parameters'] ||= {} - measure_def['parameters'].each do |parameter, value| - if !params.has_key?(parameter.intern) + @params = {} + @db = db + + # normalize parameters hash to accept either symbol or string keys + params.each do |name, value| + @params[name.to_s] = value + end + @measure_def = measure_def + @measure_def['parameters'] ||= {} + @measure_def['parameters'].each do |parameter, value| + if !@params.has_key?(parameter) raise "No value supplied for measure parameter: #{parameter}" end - @parameters[parameter.intern] = params[parameter.intern] end - ctx = V8::Context.new - ctx['year']=YEAR_IN_SECONDS - @parameters.each do |key, param| - ctx[key]=param + # if the map function is specified then replace any erb templates with their values + # taken from the supplied params + # always true for actual measures, not always true for unit tests + if (@measure_def['map_fn']) + template = ERB.new(@measure_def['map_fn']) + context = Context.new(@db, @params) + @measure_def['map_fn'] = template.result(context.get_binding) end - measure_def['calculated_dates'] ||= {} - measure_def['calculated_dates'].each do |parameter, value| - @parameters[parameter.intern]=ctx.eval(value) - end - @property_prefix = 'this.measures["'+@id+'"].' end + # Get the map function for the measure + # @return [String] the map function def map_function - "function () {\n" + - " var value = {i: 0, d: 0, n: 0, e: 0};\n" + - " if #{population} {\n" + - " value.i++;\n" + - " if #{denominator} {\n" + - " value.d++;\n" + - " if #{numerator} {\n" + - " value.n++;\n" + - " } else if #{exception} {\n" + - " value.e++;\n" + - " value.d--;\n" + - " }\n" + - " }\n" + - " }\n" + - " emit(null, value);\n" + - "};\n" + @measure_def['map_fn'] end - REDUCE_FUNCTION = <<END_OF_REDUCE_FN -function (key, values) { - var total = {i: 0, d: 0, n: 0, e: 0}; - for (var i = 0; i < values.length; i++) { - total.i += values[i].i; - total.d += values[i].d; - total.n += values[i].n; - total.e += values[i].e; - } - return total; -}; -END_OF_REDUCE_FN - + # Get the reduce function for the measure, this is a simple + # wrapper for the reduce utility function specified in + # map-reduce-utils.js + # @return [String] the reduce function def reduce_function - REDUCE_FUNCTION + 'function (key, values) { return reduce(key, values);};' end - def population - javascript(@measure_def['population']) - end - def denominator - javascript(@measure_def['denominator']) - end - - def numerator - javascript(@measure_def['numerator']) - end - - def exception - javascript(@measure_def['exception']) - end - - def javascript(expr) - if expr.has_key?('query') - # leaf node - query = expr['query'] - triple = leaf_expr(query) - property_name = munge_property_name(triple[0]) - '('+property_name+triple[1]+triple[2]+')' - elsif expr.size==1 - operator = expr.keys[0] - result = logical_expr(operator, expr[operator]) - operator = result.shift - js = '(' - result.each_with_index do |operand,index| - if index>0 - js+=operator - end - js+=operand - end - js+=')' - js - elsif expr.size==0 - '(false)' - else - throw "Unexpected number of keys in: #{expr}" - end - end - - def munge_property_name(name) - if name=='birthdate' - 'this.'+name - else - @property_prefix+name - end - end - - def logical_expr(operator, args) - operands = args.collect { |arg| javascript(arg) } - [get_operator(operator)].concat(operands) - end - - def leaf_expr(query) - property_name = query.keys[0] - property_value_expression = query[property_name] - if property_value_expression.kind_of?(Hash) - operator = property_value_expression.keys[0] - value = property_value_expression[operator] - [property_name, get_operator(operator), get_value(value)] - else - [property_name, '==', get_value(property_value_expression)] - end - end - - def get_operator(operator) - case operator - when '_eql' - '==' - when '_gt' - '>' - when '_gte' - '>=' - when '_lt' - '<' - when '_lte' - '<=' - when 'and' - '&&' - when 'or' - '||' - else - throw "Unknown operator: #{operator}" - end - end - - def get_value(value) - if value.kind_of?(String) && value[0]=='@' - @parameters[value[1..-1].intern].to_s - elsif value.kind_of?(String) - '"'+value+'"' - elsif value==nil - 'null' - else - value.to_s - end - end end end end \ No newline at end of file