Skip to content

Commit a41cc6f

Browse files
authored
Merge pull request #317 from samply/develop
v0.20.0
2 parents 2463fef + 344978f commit a41cc6f

27 files changed

+2820
-48
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
# Samply.Focus v0.20.0 2026-01-13
2+
3+
## Major changes
4+
5+
* Optional obfuscation using limited Laplace distribution and rounding to the floor(number of digits /2) significant digits
6+
* Exporter uses DKTK CQL generation
7+
8+
## Minor changes
9+
10+
* Workaround for multiple ORed empty criteria fields
11+
* BETWEEN operator suported with only one boundary defined in the numeric range
12+
* Fixed double serialization in EUCAIM SQL
13+
14+
115
# Samply.Focus v0.19.0 2025-11-03
216

317
## Major changes

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "focus"
3-
version = "0.19.0"
3+
version = "0.20.0"
44
edition = "2021"
55
license = "Apache-2.0"
66

@@ -25,7 +25,7 @@ tokio = { version = "1.25.0", default-features = false, features = [
2525
beam-lib = { git = "https://github.com/samply/beam", branch = "develop", features = [
2626
"http-util",
2727
] }
28-
laplace_rs = { git = "https://github.com/samply/laplace-rs.git", tag = "v0.5.0" }
28+
laplace_rs = { git = "https://github.com/samply/laplace-rs.git", tag = "v0.6.0" }
2929
uuid = "1.8.0"
3030
rand = { default-features = false, version = "0.8.5" }
3131
futures-util = { version = "0.3", default-features = false, features = ["std"] }

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ MAX_DB_ATTEMPTS = "8" # Max number of attempts to connect to the database; defau
6767

6868
Obfuscating zero counts is by default switched off. To enable obfuscating zero counts, set the env. variable `OBFUSCATE_ZERO`.
6969

70+
To obfuscate BBMRI-ERIC way (using a limited Laplace distribution and rounding to the floor(number of digits)/2 significant digits), set the env. variable `OBFUSCATE_BBMRI_ERIC_WAY`.
71+
7072
Optionally, you can provide the `TLS_CA_CERTIFICATES_DIR` environment variable to add additional trusted certificates, e.g., if you have a TLS-terminating proxy server in place. The application respects the `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY`, and their respective lowercase equivalents.
7173

7274
Log level can be set using the `RUST_LOG` environment variable.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
define Diagnosis:
2+
if InInitialPopulation then [Condition] else {} as List<Condition>
3+
4+
define function DiagnosisCode(condition FHIR.Condition):
5+
condition.bodySite.coding.where(system = 'http://terminology.hl7.org/CodeSystem/icd-o-3').code.first()

resources/test/result_greater.cql

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
library Retrieve
2+
using FHIR version '4.0.0'
3+
include FHIRHelpers version '4.0.0'
4+
5+
codesystem icd10: 'http://hl7.org/fhir/sid/icd-10'
6+
codesystem SampleMaterialType: 'https://fhir.bbmri.de/CodeSystem/SampleMaterialType'
7+
8+
9+
context Patient
10+
11+
define AgeClass:
12+
if (Patient.birthDate is null) then 'unknown' else ToString((AgeInYears() div 10) * 10)
13+
14+
define Gender:
15+
if (Patient.gender is null) then 'unknown' else Patient.gender
16+
17+
define Custodian:
18+
First(from Specimen.extension E
19+
where E.url = 'https://fhir.bbmri.de/StructureDefinition/Custodian'
20+
return (E.value as Reference).identifier.value)
21+
22+
define function SampleType(specimen FHIR.Specimen):
23+
case FHIRHelpers.ToCode(specimen.type.coding.where(system = 'https://fhir.bbmri.de/CodeSystem/SampleMaterialType').first())
24+
when Code 'plasma-edta' from SampleMaterialType then 'blood-plasma'
25+
when Code 'plasma-citrat' from SampleMaterialType then 'blood-plasma'
26+
when Code 'plasma-heparin' from SampleMaterialType then 'blood-plasma'
27+
when Code 'plasma-cell-free' from SampleMaterialType then 'blood-plasma'
28+
when Code 'plasma-other' from SampleMaterialType then 'blood-plasma'
29+
when Code 'plasma' from SampleMaterialType then 'blood-plasma'
30+
when Code 'tissue-formalin' from SampleMaterialType then 'tissue-ffpe'
31+
when Code 'tumor-tissue-ffpe' from SampleMaterialType then 'tissue-ffpe'
32+
when Code 'normal-tissue-ffpe' from SampleMaterialType then 'tissue-ffpe'
33+
when Code 'other-tissue-ffpe' from SampleMaterialType then 'tissue-ffpe'
34+
when Code 'tumor-tissue-frozen' from SampleMaterialType then 'tissue-frozen'
35+
when Code 'normal-tissue-frozen' from SampleMaterialType then 'tissue-frozen'
36+
when Code 'other-tissue-frozen' from SampleMaterialType then 'tissue-frozen'
37+
when Code 'tissue-paxgene-or-else' from SampleMaterialType then 'tissue-other'
38+
when Code 'derivative' from SampleMaterialType then 'derivative-other'
39+
when Code 'liquid' from SampleMaterialType then 'liquid-other'
40+
when Code 'tissue' from SampleMaterialType then 'tissue-other'
41+
when Code 'serum' from SampleMaterialType then 'blood-serum'
42+
when Code 'cf-dna' from SampleMaterialType then 'dna'
43+
when Code 'g-dna' from SampleMaterialType then 'dna'
44+
when Code 'blood-plasma' from SampleMaterialType then 'blood-plasma'
45+
when Code 'tissue-ffpe' from SampleMaterialType then 'tissue-ffpe'
46+
when Code 'tissue-frozen' from SampleMaterialType then 'tissue-frozen'
47+
when Code 'tissue-other' from SampleMaterialType then 'tissue-other'
48+
when Code 'derivative-other' from SampleMaterialType then 'derivative-other'
49+
when Code 'liquid-other' from SampleMaterialType then 'liquid-other'
50+
when Code 'blood-serum' from SampleMaterialType then 'blood-serum'
51+
when Code 'dna' from SampleMaterialType then 'dna'
52+
when Code 'buffy-coat' from SampleMaterialType then 'buffy-coat'
53+
when Code 'urine' from SampleMaterialType then 'urine'
54+
when Code 'ascites' from SampleMaterialType then 'ascites'
55+
when Code 'saliva' from SampleMaterialType then 'saliva'
56+
when Code 'csf-liquor' from SampleMaterialType then 'csf-liquor'
57+
when Code 'bone-marrow' from SampleMaterialType then 'bone-marrow'
58+
when Code 'peripheral-blood-cells-vital' from SampleMaterialType then 'peripheral-blood-cells-vital'
59+
when Code 'stool-faeces' from SampleMaterialType then 'stool-faeces'
60+
when Code 'rna' from SampleMaterialType then 'rna'
61+
when Code 'whole-blood' from SampleMaterialType then 'whole-blood'
62+
when Code 'swab' from SampleMaterialType then 'swab'
63+
when Code 'dried-whole-blood' from SampleMaterialType then 'dried-whole-blood'
64+
when null then 'Unknown'
65+
else 'Unknown'
66+
end
67+
define Specimen:
68+
if InInitialPopulation then [Specimen] S else {} as List<Specimen>
69+
70+
define Diagnosis:
71+
if InInitialPopulation then [Condition] else {} as List<Condition>
72+
73+
define function DiagnosisCode(condition FHIR.Condition):
74+
condition.code.coding.where(system = 'http://fhir.de/CodeSystem/bfarm/icd-10-gm').code.first()
75+
76+
define function DiagnosisCode(condition FHIR.Condition, specimen FHIR.Specimen):
77+
Coalesce(
78+
condition.code.coding.where(system = 'http://hl7.org/fhir/sid/icd-10').code.first(),
79+
condition.code.coding.where(system = 'http://fhir.de/CodeSystem/dimdi/icd-10-gm').code.first(),
80+
condition.code.coding.where(system = 'http://fhir.de/CodeSystem/bfarm/icd-10-gm').code.first(),
81+
specimen.extension.where(url='https://fhir.bbmri.de/StructureDefinition/SampleDiagnosis').value.coding.code.first()
82+
)
83+
84+
define InInitialPopulation:
85+
((((exists from [Condition] C
86+
where AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) >= Ceiling(60)))))

resources/test/result_less.cql

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
library Retrieve
2+
using FHIR version '4.0.0'
3+
include FHIRHelpers version '4.0.0'
4+
5+
codesystem icd10: 'http://hl7.org/fhir/sid/icd-10'
6+
codesystem SampleMaterialType: 'https://fhir.bbmri.de/CodeSystem/SampleMaterialType'
7+
8+
9+
context Patient
10+
11+
define AgeClass:
12+
if (Patient.birthDate is null) then 'unknown' else ToString((AgeInYears() div 10) * 10)
13+
14+
define Gender:
15+
if (Patient.gender is null) then 'unknown' else Patient.gender
16+
17+
define Custodian:
18+
First(from Specimen.extension E
19+
where E.url = 'https://fhir.bbmri.de/StructureDefinition/Custodian'
20+
return (E.value as Reference).identifier.value)
21+
22+
define function SampleType(specimen FHIR.Specimen):
23+
case FHIRHelpers.ToCode(specimen.type.coding.where(system = 'https://fhir.bbmri.de/CodeSystem/SampleMaterialType').first())
24+
when Code 'plasma-edta' from SampleMaterialType then 'blood-plasma'
25+
when Code 'plasma-citrat' from SampleMaterialType then 'blood-plasma'
26+
when Code 'plasma-heparin' from SampleMaterialType then 'blood-plasma'
27+
when Code 'plasma-cell-free' from SampleMaterialType then 'blood-plasma'
28+
when Code 'plasma-other' from SampleMaterialType then 'blood-plasma'
29+
when Code 'plasma' from SampleMaterialType then 'blood-plasma'
30+
when Code 'tissue-formalin' from SampleMaterialType then 'tissue-ffpe'
31+
when Code 'tumor-tissue-ffpe' from SampleMaterialType then 'tissue-ffpe'
32+
when Code 'normal-tissue-ffpe' from SampleMaterialType then 'tissue-ffpe'
33+
when Code 'other-tissue-ffpe' from SampleMaterialType then 'tissue-ffpe'
34+
when Code 'tumor-tissue-frozen' from SampleMaterialType then 'tissue-frozen'
35+
when Code 'normal-tissue-frozen' from SampleMaterialType then 'tissue-frozen'
36+
when Code 'other-tissue-frozen' from SampleMaterialType then 'tissue-frozen'
37+
when Code 'tissue-paxgene-or-else' from SampleMaterialType then 'tissue-other'
38+
when Code 'derivative' from SampleMaterialType then 'derivative-other'
39+
when Code 'liquid' from SampleMaterialType then 'liquid-other'
40+
when Code 'tissue' from SampleMaterialType then 'tissue-other'
41+
when Code 'serum' from SampleMaterialType then 'blood-serum'
42+
when Code 'cf-dna' from SampleMaterialType then 'dna'
43+
when Code 'g-dna' from SampleMaterialType then 'dna'
44+
when Code 'blood-plasma' from SampleMaterialType then 'blood-plasma'
45+
when Code 'tissue-ffpe' from SampleMaterialType then 'tissue-ffpe'
46+
when Code 'tissue-frozen' from SampleMaterialType then 'tissue-frozen'
47+
when Code 'tissue-other' from SampleMaterialType then 'tissue-other'
48+
when Code 'derivative-other' from SampleMaterialType then 'derivative-other'
49+
when Code 'liquid-other' from SampleMaterialType then 'liquid-other'
50+
when Code 'blood-serum' from SampleMaterialType then 'blood-serum'
51+
when Code 'dna' from SampleMaterialType then 'dna'
52+
when Code 'buffy-coat' from SampleMaterialType then 'buffy-coat'
53+
when Code 'urine' from SampleMaterialType then 'urine'
54+
when Code 'ascites' from SampleMaterialType then 'ascites'
55+
when Code 'saliva' from SampleMaterialType then 'saliva'
56+
when Code 'csf-liquor' from SampleMaterialType then 'csf-liquor'
57+
when Code 'bone-marrow' from SampleMaterialType then 'bone-marrow'
58+
when Code 'peripheral-blood-cells-vital' from SampleMaterialType then 'peripheral-blood-cells-vital'
59+
when Code 'stool-faeces' from SampleMaterialType then 'stool-faeces'
60+
when Code 'rna' from SampleMaterialType then 'rna'
61+
when Code 'whole-blood' from SampleMaterialType then 'whole-blood'
62+
when Code 'swab' from SampleMaterialType then 'swab'
63+
when Code 'dried-whole-blood' from SampleMaterialType then 'dried-whole-blood'
64+
when null then 'Unknown'
65+
else 'Unknown'
66+
end
67+
define Specimen:
68+
if InInitialPopulation then [Specimen] S else {} as List<Specimen>
69+
70+
define Diagnosis:
71+
if InInitialPopulation then [Condition] else {} as List<Condition>
72+
73+
define function DiagnosisCode(condition FHIR.Condition):
74+
condition.code.coding.where(system = 'http://fhir.de/CodeSystem/bfarm/icd-10-gm').code.first()
75+
76+
define function DiagnosisCode(condition FHIR.Condition, specimen FHIR.Specimen):
77+
Coalesce(
78+
condition.code.coding.where(system = 'http://hl7.org/fhir/sid/icd-10').code.first(),
79+
condition.code.coding.where(system = 'http://fhir.de/CodeSystem/dimdi/icd-10-gm').code.first(),
80+
condition.code.coding.where(system = 'http://fhir.de/CodeSystem/bfarm/icd-10-gm').code.first(),
81+
specimen.extension.where(url='https://fhir.bbmri.de/StructureDefinition/SampleDiagnosis').value.coding.code.first()
82+
)
83+
84+
define InInitialPopulation:
85+
((((exists from [Condition] C
86+
where AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) <= Ceiling(60)))))

src/ast.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ pub enum ConditionType {
2323
NotEquals,
2424
In,
2525
Between,
26-
LowerThan,
27-
GreaterThan,
28-
Contains,
2926
}
3027

3128
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -41,8 +38,8 @@ pub enum ConditionValue {
4138

4239
#[derive(Serialize, Deserialize, Debug, Clone)]
4340
pub struct NumRange {
44-
pub min: f64,
45-
pub max: f64,
41+
pub min: Option<f64>,
42+
pub max: Option<f64>,
4643
}
4744

4845
#[derive(Serialize, Deserialize, Debug, Clone)]

src/blaze.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ use crate::errors::FocusError;
1010
use crate::util;
1111
use crate::util::get_json_field;
1212

13+
#[derive(Deserialize, Debug)]
14+
#[serde(tag = "lang", rename_all = "lowercase")]
15+
pub enum Language {
16+
Cql(CqlQuery),
17+
Ast(AstQuery),
18+
}
19+
1320
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
1421
pub struct CqlQuery {
1522
pub lib: Value,

src/config.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub enum EndpointType {
2121
Blaze,
2222
Omop, // endpoint is URL of a query mediator translating AST to provider specific SQL
2323
EucaimApi, // endpoint is URL of custom API for querying EUCAIM provider
24+
EucaimBeacon,
2425
#[cfg(feature = "query-sql")]
2526
EucaimSql,
2627
#[cfg(feature = "query-sql")]
@@ -35,6 +36,7 @@ impl fmt::Display for EndpointType {
3536
EndpointType::Blaze => write!(f, "blaze"),
3637
EndpointType::Omop => write!(f, "omop"),
3738
EndpointType::EucaimApi => write!(f, "eucaim_api"),
39+
EndpointType::EucaimBeacon => write!(f, "eucaim_beacon"),
3840
#[cfg(feature = "query-sql")]
3941
EndpointType::EucaimSql => write!(f, "eucaim_sql"),
4042
#[cfg(feature = "query-sql")]
@@ -103,6 +105,10 @@ struct CliArgs {
103105
#[clap(long, env, value_parser = clap::value_parser!(Obfuscate), default_value = "yes")]
104106
obfuscate: Obfuscate,
105107

108+
/// Should the results be obfuscated the BBMRI ERIC way - default false
109+
#[clap(long, env, value_parser)]
110+
obfuscate_bbmri_eric_way: bool,
111+
106112
/// Should zero values be obfuscated - default false
107113
#[clap(long, env, value_parser)]
108114
obfuscate_zero: bool,
@@ -198,6 +204,7 @@ pub(crate) struct Config {
198204
pub endpoint_type: EndpointType,
199205
pub cql_projects_enabled: Option<Vec<String>>,
200206
pub obfuscate: Obfuscate,
207+
pub obfuscate_bbmri_eric_way: bool,
201208
pub obfuscate_zero: bool,
202209
pub obfuscate_below_10_mode: usize,
203210
pub delta_patient: f64,
@@ -252,6 +259,7 @@ impl Config {
252259
endpoint_type: cli_args.endpoint_type,
253260
cql_projects_enabled: cli_args.cql_projects_enabled,
254261
obfuscate: cli_args.obfuscate,
262+
obfuscate_bbmri_eric_way: cli_args.obfuscate_bbmri_eric_way,
255263
obfuscate_zero: cli_args.obfuscate_zero,
256264
obfuscate_below_10_mode: cli_args.obfuscate_below_10_mode,
257265
delta_patient: cli_args.delta_patient,

0 commit comments

Comments
 (0)