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.
- 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
|