canql 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.hound.yml +10 -0
- data/.rubocop.yml +10 -0
- data/.travis.yml +11 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +76 -0
- data/Rakefile +13 -0
- data/bin/console +11 -0
- data/bin/setup +7 -0
- data/canql.gemspec +39 -0
- data/code_safety.yml +201 -0
- data/lib/canql.rb +5 -0
- data/lib/canql/constants.rb +10 -0
- data/lib/canql/grammars.rb +12 -0
- data/lib/canql/grammars/age.treetop +23 -0
- data/lib/canql/grammars/anomaly.treetop +19 -0
- data/lib/canql/grammars/batch_types.treetop +11 -0
- data/lib/canql/grammars/dates.treetop +39 -0
- data/lib/canql/grammars/e_base_records.treetop +82 -0
- data/lib/canql/grammars/main.treetop +86 -0
- data/lib/canql/grammars/patient.treetop +55 -0
- data/lib/canql/grammars/registry.treetop +124 -0
- data/lib/canql/grammars/test_result.treetop +19 -0
- data/lib/canql/nodes.rb +20 -0
- data/lib/canql/nodes/age.rb +34 -0
- data/lib/canql/nodes/anomaly.rb +23 -0
- data/lib/canql/nodes/batch_types.rb +17 -0
- data/lib/canql/nodes/dates.rb +60 -0
- data/lib/canql/nodes/e_base_records.rb +60 -0
- data/lib/canql/nodes/main.rb +12 -0
- data/lib/canql/nodes/patient.rb +64 -0
- data/lib/canql/nodes/registry.rb +16 -0
- data/lib/canql/nodes/test_result.rb +21 -0
- data/lib/canql/parser.rb +41 -0
- data/lib/canql/treetop/extensions.rb +53 -0
- data/lib/canql/version.rb +6 -0
- metadata +261 -0
@@ -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
|
data/lib/canql/nodes.rb
ADDED
@@ -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
|
data/lib/canql/parser.rb
ADDED
@@ -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
|