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