canql 1.3.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.
@@ -0,0 +1,19 @@
1
+ module Canql
2
+ grammar TestResult
3
+ rule test_results
4
+ and_keyword? existance_modifier:test_result_no_keyword? natal_period:test_result_natal_period? test_results_keyword <Nodes::TestResult::Exists>
5
+ end
6
+
7
+ rule test_result_no_keyword
8
+ space ('no' / 'some') word_break
9
+ end
10
+
11
+ rule test_result_natal_period
12
+ (prenatal_keyword / postnatal_keyword)
13
+ end
14
+
15
+ rule test_results_keyword
16
+ space 'tests' word_break
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ # These file contain custom mixins for treetop
3
+ # nodes that enable them to generate meta_data_items, etc.
4
+
5
+ # Treetop documentation seems to be out of date;
6
+ # it says to create subclasses of nodes. However,
7
+ # treetop 1.4.10 expects to extend the node with
8
+ # the specified <MODULE> from the grammar.
9
+ require 'canql/treetop/extensions'
10
+ require 'canql/constants'
11
+
12
+ require 'canql/nodes/age'
13
+ require 'canql/nodes/dates'
14
+ require 'canql/nodes/e_base_records'
15
+ require 'canql/nodes/batch_types'
16
+ require 'canql/nodes/anomaly'
17
+ require 'canql/nodes/test_result'
18
+ require 'canql/nodes/patient'
19
+ require 'canql/nodes/main'
20
+ require 'canql/nodes/registry'
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module Age
5
+ module BirthDateNode
6
+ def meta_data_item
7
+ subject = reverse_scan_for_marker(:subject) == 'mother' ? 'mother' : 'patient'
8
+ range = fuzzy_date.to_daterange
9
+ {
10
+ "#{subject}.birthdate" => {
11
+ Canql::LIMITS => [
12
+ range.date1.try(:to_date).try(:iso8601), range.date2.try(:to_date).try(:iso8601)
13
+ ]
14
+ }
15
+ }
16
+ end
17
+ end
18
+
19
+ module DeathDateNode
20
+ def meta_data_item
21
+ subject = reverse_scan_for_marker(:subject) == 'mother' ? 'mother' : 'patient'
22
+ range = fuzzy_date.to_daterange
23
+ {
24
+ "#{subject}.deathdate" => {
25
+ Canql::LIMITS => [
26
+ range.date1.try(:to_date).try(:iso8601), range.date2.try(:to_date).try(:iso8601)
27
+ ]
28
+ }
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module Anomaly
5
+ module Exists
6
+ def anomaly_type
7
+ text_value = natal_period.text_value.strip
8
+ return '' if '' == text_value
9
+
10
+ ".#{text_value}"
11
+ end
12
+
13
+ def meta_data_item
14
+ {
15
+ "anomaly#{anomaly_type}.exists" => {
16
+ Canql::EQUALS => existance_modifier.text_value.strip != 'no'
17
+ }
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module BatchTypeNode
5
+ def to_type
6
+ string = respond_to?(:normalise) ? normalise : text_value
7
+ string.upcase
8
+ end
9
+ end
10
+
11
+ module PaediatricNode
12
+ def normalise
13
+ 'paediatric'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ require 'chronic'
3
+ require 'ndr_support/daterange'
4
+
5
+ module Canql #:nodoc: all
6
+ module Nodes
7
+ module FuzzyDateNode
8
+ delegate :to_daterange, to: :date
9
+ end
10
+
11
+ module SpecificDateNode
12
+ delegate :to_daterange, to: :date_fragment
13
+ end
14
+
15
+ module FragmentedDateRangeNode
16
+ def to_daterange
17
+ d1 = start.to_daterange.date1
18
+ d2 = finish.to_daterange.date2
19
+
20
+ Daterange.new(d1, d2)
21
+ end
22
+ end
23
+
24
+ module YearQuarterNode
25
+ def to_daterange
26
+ quarter = text_value[0..1]
27
+ year = text_value[3..-1]
28
+ quarters = {
29
+ 'q1': "01-04-#{year}",
30
+ 'q2': "01-07-#{year}",
31
+ 'q3': "01-10-#{year}",
32
+ 'q4': "01-01-#{year.to_i + 1}"
33
+ }
34
+ Daterange.new(quarters[quarter.to_sym].to_date,
35
+ quarters[quarter.to_sym].to_date + 3.months - 1.day)
36
+ end
37
+ end
38
+
39
+ module DateFragmentNode
40
+ delegate :to_daterange, to: :fragment
41
+ end
42
+
43
+ module DateRangeNode
44
+ def to_daterange
45
+ Daterange.new(text_value.to_s)
46
+ end
47
+ end
48
+
49
+ module ChronicDateNode
50
+ def to_daterange
51
+ chronic = Chronic.parse(text_value.to_s, context: :past, guess: false)
52
+ if chronic.instance_of?(Chronic::Span)
53
+ Daterange.new(chronic.begin, chronic.end - 1.day)
54
+ else
55
+ Daterange.new(chronic)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module EBaseRecordsNode
5
+ def meta_data_item
6
+ filter =
7
+ if types.empty?
8
+ { Canql::ALL => true }
9
+ else
10
+ { Canql::EQUALS => types.to_list }
11
+ end
12
+ { 'unprocessed_records.sources' => filter }
13
+ end
14
+ end
15
+
16
+ module BatchTypesNode
17
+ delegate :to_list, to: :allowed_types
18
+ end
19
+
20
+ module AllowedTypesNode
21
+ def to_list
22
+ list = [batch_type.to_type]
23
+ list.concat types.elements.map(&:extract_type)
24
+ end
25
+ end
26
+
27
+ module MoreTypesNode
28
+ def extract_type
29
+ batch_type.to_type
30
+ end
31
+ end
32
+
33
+ module ActionsNode
34
+ def meta_data_item
35
+ { 'action.actioninitiated' => { Canql::EQUALS => action_type.text_value.upcase.strip } }
36
+ end
37
+ end
38
+
39
+ module ActionProviderCodeNode
40
+ def meta_data_item
41
+ # default to provider
42
+ key = 'providercode'
43
+ { "action.#{key}" => { Canql::EQUALS => code.text_value.upcase } }
44
+ end
45
+ end
46
+
47
+ module ActionProviderNameNode
48
+ def meta_data_item
49
+ # default to provider
50
+ key = provider_type.text_value == 'cancer network' ? 'cn_ukacrname' : 'providername'
51
+ {
52
+ "action.#{key}" => {
53
+ Canql::BEGINS => short_desc.text_value.upcase,
54
+ :interval => interval
55
+ }
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ # require 'active_support/core_ext/object/blank'
3
+
4
+ module Canql #:nodoc: all
5
+ module Nodes
6
+ module RecordCountNode
7
+ def meta_data_item
8
+ { 'limit' => { Canql::EQUALS => number.text_value.to_i } }
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module Patient
5
+ module GenderNode
6
+ def meta_data_item
7
+ { 'patient.sex' => { Canql::EQUALS => gender.text_value == 'male' ? '1' : '2' } }
8
+ end
9
+ end
10
+
11
+ module OutcomeNode
12
+ def meta_data_item
13
+ { 'patient.outcome' => { Canql::EQUALS => outcome.text_value } }
14
+ end
15
+ end
16
+
17
+ module FieldExists
18
+ FIELDS = {
19
+ 'date of birth': { patient: 'birthdate', mother: 'birthdate' },
20
+ 'dob': { patient: 'birthdate', mother: 'birthdate' },
21
+ 'postcode': { patient: 'postcode', mother: 'postcode' },
22
+ 'nhs number': { patient: 'nhsnumber', mother: 'nhsnumber' },
23
+ 'birth weight': { patient: 'weight' },
24
+ 'place of delivery': { patient: 'placeofdelivery' },
25
+ 'sex': { patient: 'sex' },
26
+ 'outcome': { patient: 'outcome' }
27
+ }.freeze
28
+
29
+ def meta_data_item
30
+ subject = reverse_scan_for_marker(:subject) == 'mother' ? 'mother' : 'patient'
31
+ field_names = actual_field_names(
32
+ patient_field_list.text_values_for_marker(:patient_field_name), subject.to_sym
33
+ )
34
+ modifer = field_existance_modifier.text_value.strip
35
+ existance = modifer != 'missing' ? 'fields_populated' : 'fields_missing'
36
+ { "#{subject}.#{existance}" => { Canql::EQUALS => field_names } }
37
+ end
38
+
39
+ def actual_field_names(fields, subject)
40
+ actual_fields = []
41
+ fields.each do |f|
42
+ f = f.downcase.to_sym
43
+ actual_fields << FIELDS[f][subject] unless FIELDS[f].nil? || FIELDS[f][subject].nil?
44
+ end
45
+ actual_fields
46
+ end
47
+ end
48
+
49
+ module ExpectedDateRangeNode
50
+ def meta_data_item
51
+ range = fuzzy_date.to_daterange
52
+ {
53
+ 'patient.expecteddeliverydate' => {
54
+ Canql::LIMITS => [
55
+ range.date1.try(:to_date).try(:iso8601),
56
+ range.date2.try(:to_date).try(:iso8601)
57
+ ]
58
+ }
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module RegistryNode
5
+ def meta_data_item
6
+ { 'patient.registry' => { Canql::EQUALS => registry.to_registrycode } }
7
+ end
8
+ end
9
+
10
+ module RegistryCodeNode
11
+ def to_registrycode
12
+ text_value.upcase
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module TestResult
5
+ module Exists
6
+ def test_result_type
7
+ text_value = natal_period.text_value.strip
8
+ return '' if '' == text_value
9
+
10
+ ".#{text_value}"
11
+ end
12
+
13
+ def meta_data_item
14
+ { "testresults#{test_result_type}.exists" => {
15
+ Canql::EQUALS => existance_modifier.text_value.strip != 'no'
16
+ } }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ require 'canql/nodes'
3
+ require 'canql/grammars'
4
+
5
+ module Canql
6
+ # This class simplifies CANQL queries by wrapping them in a little syntactic sugar.
7
+ class Parser
8
+ attr_reader :parser
9
+
10
+ def initialize(query)
11
+ raise ArgumentError unless query.is_a?(String)
12
+
13
+ @parser = CanqlParser.new
14
+ @result = @parser.parse(query.downcase)
15
+
16
+ return if valid?
17
+ # FIXME: should log "Parser failed parsing \"#{query}\": #{@parser.failure_reason} " \
18
+ # "(line: #{@parser.failure_line}, column: #{@parser.failure_column})"
19
+ end
20
+
21
+ def valid?
22
+ !@result.nil?
23
+ end
24
+
25
+ def failure_reason
26
+ valid? ? nil : @parser.failure_reason
27
+ end
28
+
29
+ def failure_line
30
+ valid? ? nil : @parser.failure_line
31
+ end
32
+
33
+ def failure_column
34
+ valid? ? nil : @parser.failure_column
35
+ end
36
+
37
+ def meta_data
38
+ valid? ? @result.meta_data : {}
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ module Treetop
3
+ module Runtime
4
+ # We extend the SyntaxNode class to include a hash of meta data.
5
+ # Individual rules can define meta_data_item method that must return
6
+ # a hash that is merged with the hash data for the entire query.
7
+ class SyntaxNode
8
+ def meta_data(hash = {})
9
+ hash.merge!(meta_data_item) if respond_to?(:meta_data_item)
10
+
11
+ if nonterminal?
12
+ elements.each do |element|
13
+ element.meta_data(hash)
14
+ end
15
+ end
16
+
17
+ hash
18
+ end
19
+
20
+ def text_values_for_marker(marker)
21
+ list = []
22
+ text_values_for_marker_scanner(self, marker, list)
23
+ list
24
+ end
25
+
26
+ def reverse_scan_for_marker(marker)
27
+ marker_value = reverse_marker_scanner(self, marker)
28
+ return if marker_value.nil? || marker_value.empty?
29
+ marker_value
30
+ end
31
+
32
+ private
33
+
34
+ def text_values_for_marker_scanner(root, marker, list)
35
+ return if root.elements.nil?
36
+ root.elements.each do |e|
37
+ list << e.send(marker).text_value if e.respond_to?(marker)
38
+ text_values_for_marker_scanner(e, marker, list)
39
+ end
40
+ list
41
+ end
42
+
43
+ def reverse_marker_scanner(root, marker)
44
+ return if root.nil? || root.elements.nil?
45
+ marker_value = root.send(marker).text_value if root.respond_to?(marker)
46
+ if marker_value.nil? || marker_value.empty?
47
+ marker_value = reverse_marker_scanner(root.parent, marker)
48
+ end
49
+ marker_value
50
+ end
51
+ end
52
+ end
53
+ end