quality-measure-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +6 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +50 -0
- data/README.md +24 -0
- data/Rakefile +26 -0
- data/VERSION +1 -0
- data/fixtures/complex_measure.json +36 -0
- data/fixtures/result_example.json +6 -0
- data/lib/patches/v8.rb +20 -0
- data/lib/qme/map/map_reduce_builder.rb +169 -0
- data/lib/qme/map/map_reduce_executor.rb +31 -0
- data/lib/qme/query/json_document_builder.rb +124 -0
- data/lib/quality_measure_engine.rb +11 -0
- data/measures/0032/0032_NQF_Cervical_Cancer_Screening.json +171 -0
- data/measures/0032/patients/denominator1.json +10 -0
- data/measures/0032/patients/denominator2.json +10 -0
- data/measures/0032/patients/numerator1.json +11 -0
- data/measures/0032/patients/population1.json +9 -0
- data/measures/0032/patients/population2.json +11 -0
- data/measures/0032/result/result.json +6 -0
- data/measures/0043/0043_NQF_PneumoniaVaccinationStatusForOlderAdults.json +71 -0
- data/measures/0043/patients/denominator.json +11 -0
- data/measures/0043/patients/numerator.json +11 -0
- data/measures/0043/patients/population.json +10 -0
- data/measures/0043/result/result.json +6 -0
- data/quality-measure-engine.gemspec +102 -0
- data/schema/result.json +28 -0
- data/schema/schema.json +143 -0
- data/spec/qme/map/map_reduce_builder_spec.rb +64 -0
- data/spec/qme/measures_spec.rb +50 -0
- data/spec/qme/query/json_document_builder_spec.rb +56 -0
- data/spec/schema_spec.rb +21 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/validate_measures_spec.rb +21 -0
- metadata +221 -0
    
        data/.gitignore
    ADDED
    
    
    
        data/Gemfile
    ADDED
    
    | @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            source :gemcutter
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            gem 'mongo'
         | 
| 4 | 
            +
            gem 'mongomatic'
         | 
| 5 | 
            +
            gem 'bson_ext'
         | 
| 6 | 
            +
            gem 'rake'
         | 
| 7 | 
            +
            gem 'therubyracer', :require => 'v8'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            group :test do
         | 
| 10 | 
            +
              gem 'rspec'
         | 
| 11 | 
            +
              gem 'jsonschema'
         | 
| 12 | 
            +
              gem 'awesome_print', :require => 'ap'
         | 
| 13 | 
            +
            end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            group :build do
         | 
| 16 | 
            +
              gem 'jeweler'
         | 
| 17 | 
            +
            end
         | 
| 18 | 
            +
             | 
    
        data/Gemfile.lock
    ADDED
    
    | @@ -0,0 +1,50 @@ | |
| 1 | 
            +
            GEM
         | 
| 2 | 
            +
              remote: http://rubygems.org/
         | 
| 3 | 
            +
              specs:
         | 
| 4 | 
            +
                activesupport (3.0.1)
         | 
| 5 | 
            +
                awesome_print (0.2.1)
         | 
| 6 | 
            +
                bson (1.1)
         | 
| 7 | 
            +
                bson_ext (1.1.1)
         | 
| 8 | 
            +
                diff-lcs (1.1.2)
         | 
| 9 | 
            +
                gemcutter (0.6.1)
         | 
| 10 | 
            +
                git (1.2.5)
         | 
| 11 | 
            +
                jeweler (1.4.0)
         | 
| 12 | 
            +
                  gemcutter (>= 0.1.0)
         | 
| 13 | 
            +
                  git (>= 1.2.5)
         | 
| 14 | 
            +
                  rubyforge (>= 2.0.0)
         | 
| 15 | 
            +
                json_pure (1.4.6)
         | 
| 16 | 
            +
                jsonschema (2.0.0)
         | 
| 17 | 
            +
                mongo (1.1)
         | 
| 18 | 
            +
                  bson (>= 1.0.5)
         | 
| 19 | 
            +
                mongomatic (0.5.8)
         | 
| 20 | 
            +
                  activesupport (>= 2.3.5)
         | 
| 21 | 
            +
                  bson (= 1.1)
         | 
| 22 | 
            +
                  mongo (= 1.1)
         | 
| 23 | 
            +
                rake (0.8.7)
         | 
| 24 | 
            +
                rspec (2.0.0)
         | 
| 25 | 
            +
                  rspec-core (= 2.0.0)
         | 
| 26 | 
            +
                  rspec-expectations (= 2.0.0)
         | 
| 27 | 
            +
                  rspec-mocks (= 2.0.0)
         | 
| 28 | 
            +
                rspec-core (2.0.0)
         | 
| 29 | 
            +
                rspec-expectations (2.0.0)
         | 
| 30 | 
            +
                  diff-lcs (>= 1.1.2)
         | 
| 31 | 
            +
                rspec-mocks (2.0.0)
         | 
| 32 | 
            +
                  rspec-core (= 2.0.0)
         | 
| 33 | 
            +
                  rspec-expectations (= 2.0.0)
         | 
| 34 | 
            +
                rubyforge (2.0.4)
         | 
| 35 | 
            +
                  json_pure (>= 1.1.7)
         | 
| 36 | 
            +
                therubyracer (0.7.5)
         | 
| 37 | 
            +
             | 
| 38 | 
            +
            PLATFORMS
         | 
| 39 | 
            +
              ruby
         | 
| 40 | 
            +
             | 
| 41 | 
            +
            DEPENDENCIES
         | 
| 42 | 
            +
              awesome_print
         | 
| 43 | 
            +
              bson_ext
         | 
| 44 | 
            +
              jeweler
         | 
| 45 | 
            +
              jsonschema
         | 
| 46 | 
            +
              mongo
         | 
| 47 | 
            +
              mongomatic
         | 
| 48 | 
            +
              rake
         | 
| 49 | 
            +
              rspec
         | 
| 50 | 
            +
              therubyracer
         | 
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            This project will provide a library that can ingest HITSP C32's and ASTM CCR's and extract values needed to compute heath quality measures for a population. It will then be able to query over a population to compute how many people within a population conform to the measure.
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Environment
         | 
| 4 | 
            +
            -----------
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            This project currently uses Ruby 1.9.2 and is built using [Bundler](http://gembundler.com/). To get all of the dependencies for the project, first install bundler:
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                gem install bundler
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            Then run bundler to grab all of the necessay gems:
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                bundle install
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            Testing
         | 
| 15 | 
            +
            -------
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            This project uses [RSpec](http://github.com/rspec/rspec-core) for testing. To run the suite, just enter the following:
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                rake spec
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            Project Practices
         | 
| 22 | 
            +
            ------------------
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            Please try to follow our [Coding Style Guides](http://github.com/eedrummer/styleguide). Additionally, we will be using git in a pattern similar to [Vincent Driessen's workflow](http://nvie.com/posts/a-successful-git-branching-model/). While feature branches are encouraged, they are not required to work on the project.
         | 
    
        data/Rakefile
    ADDED
    
    | @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            require 'rspec/core/rake_task'
         | 
| 2 | 
            +
            require 'jeweler'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            RSpec::Core::RakeTask.new do |t|
         | 
| 5 | 
            +
              t.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"]
         | 
| 6 | 
            +
              t.pattern = 'spec/**/*_spec.rb'
         | 
| 7 | 
            +
            end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            Jeweler::Tasks.new do |gem|
         | 
| 10 | 
            +
              gem.name = "quality-measure-engine"
         | 
| 11 | 
            +
              gem.summary = "A library for extracting quality measure information from HITSP C32's and ASTM CCR's"
         | 
| 12 | 
            +
              gem.description = "A library for extracting quality measure information from HITSP C32's and ASTM CCR's"
         | 
| 13 | 
            +
              gem.email = "talk@projectpophealth.org"
         | 
| 14 | 
            +
              gem.homepage = "http://github.com/pophealth/quality-measure-engine"
         | 
| 15 | 
            +
              gem.authors = ["Marc Hadley", "Andy Gregorowicz"]
         | 
| 16 | 
            +
              
         | 
| 17 | 
            +
              gem.add_dependency 'mongo', '~> 1.1'
         | 
| 18 | 
            +
              gem.add_dependency 'mongomatic', '~> 0.5.8'
         | 
| 19 | 
            +
              gem.add_dependency 'therubyracer', '~> 0.7.5'
         | 
| 20 | 
            +
              gem.add_dependency 'bson_ext', '~> 1.1.1'
         | 
| 21 | 
            +
              
         | 
| 22 | 
            +
              gem.add_development_dependency "jsonschema", "~> 2.0.0"
         | 
| 23 | 
            +
              gem.add_development_dependency "rspec", "~> 2.0.0"
         | 
| 24 | 
            +
              gem.add_development_dependency "awesome_print", "~> 0.2.1"
         | 
| 25 | 
            +
              gem.add_development_dependency "jeweler", "~> 1.4.0"
         | 
| 26 | 
            +
            end
         | 
    
        data/VERSION
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            0.1.0
         | 
| @@ -0,0 +1,36 @@ | |
| 1 | 
            +
            {
         | 
| 2 | 
            +
              "id": "0043",
         | 
| 3 | 
            +
              "name": "Pneumonia Vaccination Status for Older Adults",
         | 
| 4 | 
            +
              "steward": "NCQA",
         | 
| 5 | 
            +
              "population": {
         | 
| 6 | 
            +
                "and": [
         | 
| 7 | 
            +
                  {
         | 
| 8 | 
            +
                    "category": "Patient Characteristic",
         | 
| 9 | 
            +
                    "title": "Age > 17 before measure period",
         | 
| 10 | 
            +
                    "query": {"age": {"_gt": 17}}
         | 
| 11 | 
            +
                  },
         | 
| 12 | 
            +
                  {
         | 
| 13 | 
            +
                    "category": "Patient Characteristic",
         | 
| 14 | 
            +
                    "title": "Age < 75 before measure period",
         | 
| 15 | 
            +
                    "query": {"age": {"_lt": 75}}
         | 
| 16 | 
            +
                  },
         | 
| 17 | 
            +
                  {
         | 
| 18 | 
            +
                    "or": [
         | 
| 19 | 
            +
                      {
         | 
| 20 | 
            +
                        "category": "Patient Characteristic",
         | 
| 21 | 
            +
                        "title": "Male",
         | 
| 22 | 
            +
                        "query": {"sex": "male"}
         | 
| 23 | 
            +
                      },
         | 
| 24 | 
            +
                      {
         | 
| 25 | 
            +
                        "category": "Patient Characteristic",
         | 
| 26 | 
            +
                        "title": "Female",
         | 
| 27 | 
            +
                        "query": {"sex": "female"}
         | 
| 28 | 
            +
                      }
         | 
| 29 | 
            +
                    ]
         | 
| 30 | 
            +
                  }
         | 
| 31 | 
            +
                ]
         | 
| 32 | 
            +
              },
         | 
| 33 | 
            +
              "denominator": {},
         | 
| 34 | 
            +
              "numerator": {},
         | 
| 35 | 
            +
              "exception": {}
         | 
| 36 | 
            +
            }
         | 
    
        data/lib/patches/v8.rb
    ADDED
    
    | @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            # Patching V8::Object because it is the entry point for conversions
         | 
| 2 | 
            +
            # between Ruby and JavaScript types. We are using seconds since the
         | 
| 3 | 
            +
            # epoch to represent dates. On 32 bit architectures, for recent dates
         | 
| 4 | 
            +
            # this will be too large for Fixnum and become a Bignum. Ruby Racer
         | 
| 5 | 
            +
            # worn't properly convert a Bignum to JavaScript, but it will work
         | 
| 6 | 
            +
            # just fine for a Float. Because of this, we will convert all Bignums
         | 
| 7 | 
            +
            # passed into a JavaScript context to a Float
         | 
| 8 | 
            +
            module V8
         | 
| 9 | 
            +
              class Object
         | 
| 10 | 
            +
                alias :old_index_setter :'[]='
         | 
| 11 | 
            +
                
         | 
| 12 | 
            +
                def []=(key, value)
         | 
| 13 | 
            +
                  if value.kind_of?(Bignum)
         | 
| 14 | 
            +
                    old_index_setter(key, value.to_f)
         | 
| 15 | 
            +
                  else
         | 
| 16 | 
            +
                    old_index_setter(key, value)
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
            end
         | 
| @@ -0,0 +1,169 @@ | |
| 1 | 
            +
            module QME
         | 
| 2 | 
            +
              module MapReduce
         | 
| 3 | 
            +
                class Builder
         | 
| 4 | 
            +
                  attr_reader :id, :parameters
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  YEAR_IN_SECONDS = 365*24*60*60
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def initialize(measure_def, params)
         | 
| 9 | 
            +
                    @measure_def = measure_def
         | 
| 10 | 
            +
                    @id = measure_def['id']
         | 
| 11 | 
            +
                    @parameters = {}
         | 
| 12 | 
            +
                    measure_def['parameters'] ||= {}
         | 
| 13 | 
            +
                    measure_def['parameters'].each do |parameter, value|
         | 
| 14 | 
            +
                      if !params.has_key?(parameter.intern)
         | 
| 15 | 
            +
                        raise "No value supplied for measure parameter: #{parameter}"
         | 
| 16 | 
            +
                      end
         | 
| 17 | 
            +
                      @parameters[parameter.intern] = params[parameter.intern]
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
                    ctx = V8::Context.new
         | 
| 20 | 
            +
                    ctx['year']=YEAR_IN_SECONDS
         | 
| 21 | 
            +
                    @parameters.each do |key, param|
         | 
| 22 | 
            +
                      ctx[key]=param
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
                    measure_def['calculated_dates'] ||= {}
         | 
| 25 | 
            +
                    measure_def['calculated_dates'].each do |parameter, value|
         | 
| 26 | 
            +
                      @parameters[parameter.intern]=ctx.eval(value)
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
                    @property_prefix = 'this.measures["'+@id+'"].'
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def map_function
         | 
| 32 | 
            +
                    "function () {\n" +
         | 
| 33 | 
            +
                    "  var value = {i: 0, d: 0, n: 0, e: 0};\n" +
         | 
| 34 | 
            +
                    "  if #{population} {\n" +
         | 
| 35 | 
            +
                    "    value.i++;\n" +
         | 
| 36 | 
            +
                    "    if #{denominator} {\n" +
         | 
| 37 | 
            +
                    "      value.d++;\n" +
         | 
| 38 | 
            +
                    "      if #{numerator} {\n" +
         | 
| 39 | 
            +
                    "        value.n++;\n" +
         | 
| 40 | 
            +
                    "      } else if #{exception} {\n" +
         | 
| 41 | 
            +
                    "        value.e++;\n" +
         | 
| 42 | 
            +
                    "        value.d--;\n" +
         | 
| 43 | 
            +
                    "      }\n" +
         | 
| 44 | 
            +
                    "    }\n" +
         | 
| 45 | 
            +
                    "  }\n" +
         | 
| 46 | 
            +
                    "  emit(null, value);\n" +
         | 
| 47 | 
            +
                    "};\n"
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  REDUCE_FUNCTION = <<END_OF_REDUCE_FN
         | 
| 51 | 
            +
            function (key, values) {
         | 
| 52 | 
            +
              var total = {i: 0, d: 0, n: 0, e: 0};
         | 
| 53 | 
            +
              for (var i = 0; i < values.length; i++) {
         | 
| 54 | 
            +
                total.i += values[i].i;
         | 
| 55 | 
            +
                total.d += values[i].d;
         | 
| 56 | 
            +
                total.n += values[i].n;
         | 
| 57 | 
            +
                total.e += values[i].e;
         | 
| 58 | 
            +
              }
         | 
| 59 | 
            +
              return total;
         | 
| 60 | 
            +
            };
         | 
| 61 | 
            +
            END_OF_REDUCE_FN
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  def reduce_function
         | 
| 64 | 
            +
                    REDUCE_FUNCTION
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  def population
         | 
| 68 | 
            +
                    javascript(@measure_def['population'])
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  def denominator
         | 
| 72 | 
            +
                    javascript(@measure_def['denominator'])
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  def numerator
         | 
| 76 | 
            +
                    javascript(@measure_def['numerator'])
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  def exception
         | 
| 80 | 
            +
                    javascript(@measure_def['exception'])
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  def javascript(expr)
         | 
| 84 | 
            +
                    if expr.has_key?('query')
         | 
| 85 | 
            +
                      # leaf node
         | 
| 86 | 
            +
                      query = expr['query']
         | 
| 87 | 
            +
                      triple = leaf_expr(query)
         | 
| 88 | 
            +
                      property_name = munge_property_name(triple[0])
         | 
| 89 | 
            +
                      '('+property_name+triple[1]+triple[2]+')'
         | 
| 90 | 
            +
                    elsif expr.size==1
         | 
| 91 | 
            +
                      operator = expr.keys[0]
         | 
| 92 | 
            +
                      result = logical_expr(operator, expr[operator])
         | 
| 93 | 
            +
                      operator = result.shift
         | 
| 94 | 
            +
                      js = '('
         | 
| 95 | 
            +
                      result.each_with_index do |operand,index|
         | 
| 96 | 
            +
                        if index>0
         | 
| 97 | 
            +
                          js+=operator
         | 
| 98 | 
            +
                        end
         | 
| 99 | 
            +
                        js+=operand
         | 
| 100 | 
            +
                      end
         | 
| 101 | 
            +
                      js+=')'
         | 
| 102 | 
            +
                      js
         | 
| 103 | 
            +
                    elsif expr.size==0
         | 
| 104 | 
            +
                      '(false)'
         | 
| 105 | 
            +
                    else
         | 
| 106 | 
            +
                      throw "Unexpected number of keys in: #{expr}"
         | 
| 107 | 
            +
                    end
         | 
| 108 | 
            +
                  end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                  def munge_property_name(name)
         | 
| 111 | 
            +
                    if name=='birthdate'
         | 
| 112 | 
            +
                      'this.'+name
         | 
| 113 | 
            +
                    else
         | 
| 114 | 
            +
                      @property_prefix+name
         | 
| 115 | 
            +
                    end
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                  def logical_expr(operator, args)
         | 
| 119 | 
            +
                    operands = args.collect { |arg| javascript(arg) }
         | 
| 120 | 
            +
                    [get_operator(operator)].concat(operands)
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  def leaf_expr(query)
         | 
| 124 | 
            +
                    property_name = query.keys[0]
         | 
| 125 | 
            +
                    property_value_expression = query[property_name]
         | 
| 126 | 
            +
                    if property_value_expression.kind_of?(Hash)
         | 
| 127 | 
            +
                      operator = property_value_expression.keys[0]
         | 
| 128 | 
            +
                      value = property_value_expression[operator]
         | 
| 129 | 
            +
                      [property_name, get_operator(operator), get_value(value)]
         | 
| 130 | 
            +
                    else
         | 
| 131 | 
            +
                      [property_name, '==', get_value(property_value_expression)]
         | 
| 132 | 
            +
                    end
         | 
| 133 | 
            +
                  end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                  def get_operator(operator)
         | 
| 136 | 
            +
                    case operator
         | 
| 137 | 
            +
                    when '_eql'
         | 
| 138 | 
            +
                      '=='
         | 
| 139 | 
            +
                    when '_gt'
         | 
| 140 | 
            +
                      '>'
         | 
| 141 | 
            +
                    when '_gte'
         | 
| 142 | 
            +
                      '>='
         | 
| 143 | 
            +
                    when '_lt'
         | 
| 144 | 
            +
                      '<'
         | 
| 145 | 
            +
                    when '_lte'
         | 
| 146 | 
            +
                      '<='
         | 
| 147 | 
            +
                    when 'and'
         | 
| 148 | 
            +
                      '&&'
         | 
| 149 | 
            +
                    when 'or'
         | 
| 150 | 
            +
                      '||'
         | 
| 151 | 
            +
                    else
         | 
| 152 | 
            +
                      throw "Unknown operator: #{operator}"
         | 
| 153 | 
            +
                    end
         | 
| 154 | 
            +
                  end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                  def get_value(value)
         | 
| 157 | 
            +
                    if value.kind_of?(String) && value[0]=='@'
         | 
| 158 | 
            +
                      @parameters[value[1..-1].intern].to_s
         | 
| 159 | 
            +
                    elsif value.kind_of?(String)
         | 
| 160 | 
            +
                      '"'+value+'"'
         | 
| 161 | 
            +
                    elsif value==nil
         | 
| 162 | 
            +
                      'null'
         | 
| 163 | 
            +
                    else
         | 
| 164 | 
            +
                      value.to_s
         | 
| 165 | 
            +
                    end
         | 
| 166 | 
            +
                  end
         | 
| 167 | 
            +
                end
         | 
| 168 | 
            +
              end
         | 
| 169 | 
            +
            end
         | 
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            module QME
         | 
| 2 | 
            +
              module MapReduce
         | 
| 3 | 
            +
                class Executor
         | 
| 4 | 
            +
                  def initialize(db)
         | 
| 5 | 
            +
                    @db = db
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def measure_def(measure_id)
         | 
| 9 | 
            +
                    measures = @db.collection('measures')
         | 
| 10 | 
            +
                    measures.find({'id'=> "#{measure_id}"}).to_a[0]
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def measure_result(measure_id, parameter_values)
         | 
| 14 | 
            +
                    
         | 
| 15 | 
            +
                    measure = Builder.new(measure_def(measure_id), parameter_values)
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    records = @db.collection('records')
         | 
| 18 | 
            +
                    results = records.map_reduce(measure.map_function, measure.reduce_function)
         | 
| 19 | 
            +
                    result = results.find.to_a[0]
         | 
| 20 | 
            +
                    value = result['value']
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    {
         | 
| 23 | 
            +
                      :population=>value['i'].to_i,
         | 
| 24 | 
            +
                      :denominator=> value['d'].to_i,
         | 
| 25 | 
            +
                      :numerator=> value['n'].to_i,
         | 
| 26 | 
            +
                      :exceptions=> value['e'].to_i
         | 
| 27 | 
            +
                    }
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
| @@ -0,0 +1,124 @@ | |
| 1 | 
            +
            module QME
         | 
| 2 | 
            +
              module Query
         | 
| 3 | 
            +
                class JSONDocumentBuilder
         | 
| 4 | 
            +
                  attr_accessor :parameters, :calculated_dates
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  # Creates the JSONDocumentBuilder. Will calculate dates if parameters
         | 
| 7 | 
            +
                  # are passed in.
         | 
| 8 | 
            +
                  def initialize(measure_json, parameters={})
         | 
| 9 | 
            +
                    @measure_json = measure_json
         | 
| 10 | 
            +
                    @parameters = parameters
         | 
| 11 | 
            +
                    @measure_id = measure_json['id']
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    if ! parameters.empty?
         | 
| 14 | 
            +
                      calculate_dates
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  # Calculates all dates necessary to create a query for this measure
         | 
| 19 | 
            +
                  # This will be run by the constructor if params were passed in
         | 
| 20 | 
            +
                  def calculate_dates
         | 
| 21 | 
            +
                    ctx = V8::Context.new
         | 
| 22 | 
            +
                    @parameters.each_pair do |key, value|
         | 
| 23 | 
            +
                      ctx[key] = value
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    ctx['year'] = 365 * 24 * 60 * 60 # TODO: Replace this with a js file that has all constants
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    @calculated_dates = {}
         | 
| 29 | 
            +
                    @measure_json["calculated_dates"].each_pair do |key, value|
         | 
| 30 | 
            +
                      @calculated_dates[key] = ctx.eval(value)
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  def numerator_query
         | 
| 35 | 
            +
                    create_query(@measure_json['numerator']).merge(denominator_query)
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                  
         | 
| 38 | 
            +
                  def denominator_query
         | 
| 39 | 
            +
                    create_query(@measure_json['denominator']).merge(initial_population_query)
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                  
         | 
| 42 | 
            +
                  def initial_population_query
         | 
| 43 | 
            +
                    create_query(@measure_json['population'])
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                  
         | 
| 46 | 
            +
                  def exclusions_query
         | 
| 47 | 
            +
                    create_query(@measure_json['exception'])
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  # Creates the appropriate JSON document to query MongoDB based on
         | 
| 51 | 
            +
                  # a measure definition passed in. This method calls itself recursively
         | 
| 52 | 
            +
                  # to walk the tree and get all possible logical operators available
         | 
| 53 | 
            +
                  # in a measure definition
         | 
| 54 | 
            +
                  def create_query(definition_json, args={})
         | 
| 55 | 
            +
                    if definition_json.has_key?('and')
         | 
| 56 | 
            +
                      definition_json['and'].each do |operand|
         | 
| 57 | 
            +
                        create_query(operand, args)
         | 
| 58 | 
            +
                      end
         | 
| 59 | 
            +
                    elsif definition_json.has_key?('or')
         | 
| 60 | 
            +
                      operands = []
         | 
| 61 | 
            +
                      definition_json['or'].each do |operand|
         | 
| 62 | 
            +
                        operands << create_query(operand)
         | 
| 63 | 
            +
                      end
         | 
| 64 | 
            +
                      args['$or'] = operands
         | 
| 65 | 
            +
                    elsif definition_json.has_key?('query')
         | 
| 66 | 
            +
                      process_query(definition_json['query'], args)
         | 
| 67 | 
            +
                    end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    args
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  # Called by create_query to process leaf nodes in a measure
         | 
| 73 | 
            +
                  # definition
         | 
| 74 | 
            +
                  def process_query(definition_json, args)
         | 
| 75 | 
            +
                    if definition_json.size > 1
         | 
| 76 | 
            +
                      raise 'A query should have only one property'
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                    query_property = definition_json.keys.first
         | 
| 80 | 
            +
                    document_key = transform_query_property(query_property)
         | 
| 81 | 
            +
                    document_value = nil
         | 
| 82 | 
            +
                    query_value = definition_json[query_property]
         | 
| 83 | 
            +
                    if query_value.kind_of?(Hash)
         | 
| 84 | 
            +
                      if query_value.size > 1
         | 
| 85 | 
            +
                        raise 'A query value should only have one property'
         | 
| 86 | 
            +
                      end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                      document_value = {query_value.keys.first.gsub('_', '$') =>
         | 
| 89 | 
            +
                                        substitute_variables(query_value.values.first)}
         | 
| 90 | 
            +
                      if args[document_key]
         | 
| 91 | 
            +
                        args[document_key].merge!(document_value)
         | 
| 92 | 
            +
                      else
         | 
| 93 | 
            +
                        args[document_key] = document_value
         | 
| 94 | 
            +
                      end
         | 
| 95 | 
            +
                    else
         | 
| 96 | 
            +
                      document_value = substitute_variables(query_value)
         | 
| 97 | 
            +
                      args[document_key] = document_value
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
                  
         | 
| 101 | 
            +
                  # Takes a query property name and transforms it into
         | 
| 102 | 
            +
                  # a name of a key in a MongoDB document
         | 
| 103 | 
            +
                  def transform_query_property(property_name)
         | 
| 104 | 
            +
                    #TODO What do we do with special case fields - the stuff we are keeping at the patient level?
         | 
| 105 | 
            +
                    if ['birthdate'].include?(property_name)
         | 
| 106 | 
            +
                      property_name
         | 
| 107 | 
            +
                    else
         | 
| 108 | 
            +
                      "measures.#{@measure_id}.#{property_name}"
         | 
| 109 | 
            +
                    end
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                  # Finds strings that start with "@" and replaces them
         | 
| 113 | 
            +
                  # with the calculated date
         | 
| 114 | 
            +
                  def substitute_variables(value)
         | 
| 115 | 
            +
                    if value.kind_of?(String) && value[0] == '@'
         | 
| 116 | 
            +
                      variable_name = value[1..-1]
         | 
| 117 | 
            +
                      @calculated_dates[variable_name]
         | 
| 118 | 
            +
                    else
         | 
| 119 | 
            +
                      value
         | 
| 120 | 
            +
                    end
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
              end
         | 
| 124 | 
            +
            end
         |