use serde_json::Value;
use std::collections::{BTreeMap, HashSet};
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub struct Span {
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Severity {
Error,
Warning,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum DiagnosticCode {
NotAViewDefinition,
UnknownKey,
MissingRequired,
WrongType,
EmptyRequired,
DuplicateColumnName,
MultipleIterationDirectives,
SelectWithoutOutput,
#[serde(rename = "fhirpath-syntax")]
FhirPathSyntax,
UndeclaredConstant,
UnknownResourceType,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum Fix {
RenameKey { pointer: String, to: String },
RemoveKey { pointer: String },
SetString { pointer: String, value: String },
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Diagnostic {
pub pointer: String,
pub message: String,
pub severity: Severity,
pub code: DiagnosticCode,
pub span: Option<Span>,
pub args: BTreeMap<String, String>,
pub fixes: Vec<Fix>,
}
#[derive(Clone, Copy)]
enum Kind {
String,
Number,
Boolean,
StringArray,
Object(Node),
ObjectArray(Node),
Any,
}
impl Kind {
fn is_primitive(self) -> bool {
matches!(
self,
Kind::String | Kind::Number | Kind::Boolean | Kind::StringArray
)
}
}
struct Field {
key: &'static str,
required: bool,
kind: Kind,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Node {
Root,
Select,
Column,
Tag,
Where,
Constant,
}
impl Node {
fn fields(self) -> &'static [Field] {
match self {
Node::Root => &[
Field {
key: "resourceType",
required: true,
kind: Kind::String,
},
Field {
key: "id",
required: false,
kind: Kind::Any,
},
Field {
key: "meta",
required: false,
kind: Kind::Any,
},
Field {
key: "implicitRules",
required: false,
kind: Kind::String,
},
Field {
key: "language",
required: false,
kind: Kind::String,
},
Field {
key: "text",
required: false,
kind: Kind::Any,
},
Field {
key: "contained",
required: false,
kind: Kind::Any,
},
Field {
key: "extension",
required: false,
kind: Kind::Any,
},
Field {
key: "modifierExtension",
required: false,
kind: Kind::Any,
},
Field {
key: "url",
required: false,
kind: Kind::String,
},
Field {
key: "identifier",
required: false,
kind: Kind::Any,
},
Field {
key: "version",
required: false,
kind: Kind::String,
},
Field {
key: "versionAlgorithmString",
required: false,
kind: Kind::String,
},
Field {
key: "versionAlgorithmCoding",
required: false,
kind: Kind::Any,
},
Field {
key: "name",
required: false,
kind: Kind::String,
},
Field {
key: "title",
required: false,
kind: Kind::String,
},
Field {
key: "status",
required: false,
kind: Kind::String,
},
Field {
key: "experimental",
required: false,
kind: Kind::Boolean,
},
Field {
key: "date",
required: false,
kind: Kind::String,
},
Field {
key: "publisher",
required: false,
kind: Kind::String,
},
Field {
key: "contact",
required: false,
kind: Kind::Any,
},
Field {
key: "description",
required: false,
kind: Kind::String,
},
Field {
key: "useContext",
required: false,
kind: Kind::Any,
},
Field {
key: "jurisdiction",
required: false,
kind: Kind::Any,
},
Field {
key: "purpose",
required: false,
kind: Kind::String,
},
Field {
key: "copyright",
required: false,
kind: Kind::String,
},
Field {
key: "copyrightLabel",
required: false,
kind: Kind::String,
},
Field {
key: "approvalDate",
required: false,
kind: Kind::String,
},
Field {
key: "lastReviewDate",
required: false,
kind: Kind::String,
},
Field {
key: "effectivePeriod",
required: false,
kind: Kind::Any,
},
Field {
key: "topic",
required: false,
kind: Kind::Any,
},
Field {
key: "author",
required: false,
kind: Kind::Any,
},
Field {
key: "editor",
required: false,
kind: Kind::Any,
},
Field {
key: "reviewer",
required: false,
kind: Kind::Any,
},
Field {
key: "endorser",
required: false,
kind: Kind::Any,
},
Field {
key: "relatedArtifact",
required: false,
kind: Kind::Any,
},
Field {
key: "resource",
required: true,
kind: Kind::String,
},
Field {
key: "profile",
required: false,
kind: Kind::Any,
},
Field {
key: "fhirVersion",
required: false,
kind: Kind::Any,
},
Field {
key: "constant",
required: false,
kind: Kind::ObjectArray(Node::Constant),
},
Field {
key: "select",
required: true,
kind: Kind::ObjectArray(Node::Select),
},
Field {
key: "where",
required: false,
kind: Kind::ObjectArray(Node::Where),
},
],
Node::Select => &[
Field {
key: "column",
required: false,
kind: Kind::ObjectArray(Node::Column),
},
Field {
key: "select",
required: false,
kind: Kind::ObjectArray(Node::Select),
},
Field {
key: "forEach",
required: false,
kind: Kind::String,
},
Field {
key: "forEachOrNull",
required: false,
kind: Kind::String,
},
Field {
key: "repeat",
required: false,
kind: Kind::StringArray,
},
Field {
key: "unionAll",
required: false,
kind: Kind::ObjectArray(Node::Select),
},
],
Node::Column => &[
Field {
key: "path",
required: true,
kind: Kind::String,
},
Field {
key: "name",
required: true,
kind: Kind::String,
},
Field {
key: "description",
required: false,
kind: Kind::String,
},
Field {
key: "collection",
required: false,
kind: Kind::Boolean,
},
Field {
key: "type",
required: false,
kind: Kind::String,
},
Field {
key: "tag",
required: false,
kind: Kind::ObjectArray(Node::Tag),
},
],
Node::Tag => &[
Field {
key: "name",
required: true,
kind: Kind::String,
},
Field {
key: "value",
required: true,
kind: Kind::String,
},
],
Node::Where => &[
Field {
key: "path",
required: true,
kind: Kind::String,
},
Field {
key: "description",
required: false,
kind: Kind::String,
},
],
Node::Constant => &[
Field {
key: "name",
required: true,
kind: Kind::String,
},
Field {
key: "valueBase64Binary",
required: false,
kind: Kind::String,
},
Field {
key: "valueBoolean",
required: false,
kind: Kind::Boolean,
},
Field {
key: "valueCanonical",
required: false,
kind: Kind::String,
},
Field {
key: "valueCode",
required: false,
kind: Kind::String,
},
Field {
key: "valueDate",
required: false,
kind: Kind::String,
},
Field {
key: "valueDateTime",
required: false,
kind: Kind::String,
},
Field {
key: "valueDecimal",
required: false,
kind: Kind::Number,
},
Field {
key: "valueId",
required: false,
kind: Kind::String,
},
Field {
key: "valueInstant",
required: false,
kind: Kind::String,
},
Field {
key: "valueInteger",
required: false,
kind: Kind::Number,
},
Field {
key: "valueOid",
required: false,
kind: Kind::String,
},
Field {
key: "valueString",
required: false,
kind: Kind::String,
},
Field {
key: "valuePositiveInt",
required: false,
kind: Kind::Number,
},
Field {
key: "valueTime",
required: false,
kind: Kind::String,
},
Field {
key: "valueUnsignedInt",
required: false,
kind: Kind::Number,
},
Field {
key: "valueUri",
required: false,
kind: Kind::String,
},
Field {
key: "valueUrl",
required: false,
kind: Kind::String,
},
Field {
key: "valueUuid",
required: false,
kind: Kind::String,
},
],
}
}
}
pub use helios_fhirpath::{
FunctionCategory, FunctionInfo, builtin_functions, environment_variables,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyKind {
String,
Number,
Boolean,
StringArray,
Object,
ObjectArray,
Other,
}
impl From<Kind> for KeyKind {
fn from(kind: Kind) -> Self {
match kind {
Kind::String => KeyKind::String,
Kind::Number => KeyKind::Number,
Kind::Boolean => KeyKind::Boolean,
Kind::StringArray => KeyKind::StringArray,
Kind::Object(_) => KeyKind::Object,
Kind::ObjectArray(_) => KeyKind::ObjectArray,
Kind::Any => KeyKind::Other,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyInfo {
pub key: &'static str,
pub required: bool,
pub kind: KeyKind,
}
pub fn node_keys(pointer: &str) -> Option<Vec<KeyInfo>> {
let mut node = Node::Root;
for raw_segment in pointer.split('/').skip(1) {
let segment = unescape_pointer_segment(raw_segment);
if !segment.is_empty() && segment.bytes().all(|b| b.is_ascii_digit()) {
continue;
}
let field = node.fields().iter().find(|f| f.key == segment.as_ref())?;
node = match field.kind {
Kind::Object(inner) | Kind::ObjectArray(inner) => inner,
Kind::String | Kind::Number | Kind::Boolean | Kind::StringArray | Kind::Any => {
return None;
}
};
}
Some(
node.fields()
.iter()
.map(|f| KeyInfo {
key: f.key,
required: f.required,
kind: f.kind.into(),
})
.collect(),
)
}
const SQL_ON_FHIR_ENVIRONMENT_VARIABLES: &[&str] = &["rowIndex"];
const CONSTANT_VALUE_KEYS: &[&str] = &[
"valueBase64Binary",
"valueBoolean",
"valueCanonical",
"valueCode",
"valueDate",
"valueDateTime",
"valueDecimal",
"valueId",
"valueInstant",
"valueInteger",
"valueOid",
"valueString",
"valuePositiveInt",
"valueTime",
"valueUnsignedInt",
"valueUri",
"valueUrl",
"valueUuid",
];
fn known_resource_types() -> &'static [&'static str] {
static CACHE: OnceLock<Vec<&'static str>> = OnceLock::new();
CACHE
.get_or_init(|| {
#[allow(unused_mut)]
let mut names: Vec<&'static str> = Vec::new();
#[cfg(feature = "R4")]
names.extend(
<helios_fhir::r4::Resource as helios_fhir::FhirResourceTypeProvider>::get_resource_type_names(
),
);
#[cfg(feature = "R4B")]
names.extend(
<helios_fhir::r4b::Resource as helios_fhir::FhirResourceTypeProvider>::get_resource_type_names(
),
);
#[cfg(feature = "R5")]
names.extend(
<helios_fhir::r5::Resource as helios_fhir::FhirResourceTypeProvider>::get_resource_type_names(
),
);
#[cfg(feature = "R6")]
names.extend(
<helios_fhir::r6::Resource as helios_fhir::FhirResourceTypeProvider>::get_resource_type_names(
),
);
names.sort_unstable();
names.dedup();
names
})
.as_slice()
}
pub fn lint_view_definition(doc: &Value) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let Some(root) = doc.as_object() else {
diagnostics.push(not_a_view_definition(
"a ViewDefinition document must be a JSON object".to_string(),
"a non-object document".to_string(),
));
return diagnostics;
};
let resource_type = root.get("resourceType").and_then(Value::as_str);
if resource_type != Some("ViewDefinition") {
let found = match root.get("resourceType") {
None => "no `resourceType`".to_string(),
Some(Value::String(s)) => format!("resourceType {s:?}"),
Some(other) => format!("a non-string resourceType ({})", type_name(other)),
};
diagnostics.push(not_a_view_definition(
format!("expected resourceType \"ViewDefinition\", found {found}"),
found,
));
return diagnostics;
}
validate_node(Node::Root, doc, "", &mut diagnostics);
if let Some(Value::String(resource)) = root.get("resource")
&& !resource.trim().is_empty()
&& known_resource_types()
.binary_search(&resource.as_str())
.is_err()
{
diagnostics.push(unknown_resource_type(resource));
}
if let Some(constants) = root.get("constant").and_then(Value::as_array) {
for (i, constant) in constants.iter().enumerate() {
check_constant_value(constant, &format!("/constant/{i}"), &mut diagnostics);
}
}
let declared_constants: HashSet<&str> = root
.get("constant")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(|c| c.get("name").and_then(Value::as_str))
.collect();
if let Some(selects) = root.get("select").and_then(Value::as_array) {
let mut used_names: HashSet<String> = HashSet::new();
for select in selects {
collect_column_names(select, &mut used_names);
}
let mut column_scope = Vec::new();
for (i, select) in selects.iter().enumerate() {
let pointer = format!("/select/{i}");
check_select_shape(select, &pointer, &mut diagnostics);
column_scope = check_duplicate_columns(
select,
&pointer,
column_scope,
&mut used_names,
&mut diagnostics,
);
check_fhirpath_in_select(select, &pointer, &declared_constants, &mut diagnostics);
}
}
if let Some(wheres) = root.get("where").and_then(Value::as_array) {
for (i, w) in wheres.iter().enumerate() {
if let Some(path) = w.get("path").and_then(Value::as_str) {
check_expression(
path,
&format!("/where/{i}/path"),
&declared_constants,
&mut diagnostics,
);
}
}
}
diagnostics.sort_by_cached_key(|d| {
(
document_position(doc, &d.pointer),
d.span.map(|span| span.start).unwrap_or(0),
)
});
diagnostics
}
pub fn pointer_to_fhirpath(pointer: &str) -> String {
let mut expression = String::from("ViewDefinition");
if pointer.is_empty() {
return expression;
}
for segment in pointer.split('/').skip(1) {
let key = segment.replace("~1", "/").replace("~0", "~");
if key.as_bytes().iter().all(u8::is_ascii_digit) && !key.is_empty() {
expression.push('[');
expression.push_str(&key);
expression.push(']');
} else {
expression.push('.');
expression.push_str(&key);
}
}
expression
}
pub const LINT_DIAGNOSTIC_CODING_SYSTEM: &str =
"http://heliossoftware.com/fhir/CodeSystem/view-definition-lint";
pub fn lint_operation_outcome(diagnostics: &[Diagnostic]) -> Value {
let issues: Vec<Value> = diagnostics
.iter()
.filter(|diagnostic| diagnostic.severity == Severity::Error)
.map(diagnostic_issue)
.collect();
serde_json::json!({
"resourceType": "OperationOutcome",
"issue": issues,
})
}
fn diagnostic_issue(diagnostic: &Diagnostic) -> Value {
serde_json::json!({
"severity": diagnostic.severity,
"code": issue_code(diagnostic.code),
"diagnostics": diagnostic.message,
"details": {
"text": diagnostic.message,
"coding": [{
"system": LINT_DIAGNOSTIC_CODING_SYSTEM,
"code": diagnostic_coding_code(diagnostic.code),
}],
},
"expression": [pointer_to_fhirpath(&diagnostic.pointer)],
})
}
fn issue_code(code: DiagnosticCode) -> &'static str {
match code {
DiagnosticCode::NotAViewDefinition
| DiagnosticCode::UnknownKey
| DiagnosticCode::WrongType => "structure",
DiagnosticCode::MissingRequired | DiagnosticCode::EmptyRequired => "required",
DiagnosticCode::DuplicateColumnName
| DiagnosticCode::MultipleIterationDirectives
| DiagnosticCode::SelectWithoutOutput
| DiagnosticCode::FhirPathSyntax
| DiagnosticCode::UndeclaredConstant => "invalid",
DiagnosticCode::UnknownResourceType => "code-invalid",
}
}
fn diagnostic_coding_code(code: DiagnosticCode) -> String {
match serde_json::to_value(code) {
Ok(Value::String(code)) => code,
_ => unreachable!("DiagnosticCode serializes to a JSON string"),
}
}
fn type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "a boolean",
Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Array(_) => "an array",
Value::Object(_) => "an object",
}
}
fn not_a_view_definition(message: String, found: String) -> Diagnostic {
let mut args = BTreeMap::new();
args.insert("found".to_string(), found);
Diagnostic {
pointer: String::new(),
message,
severity: Severity::Error,
code: DiagnosticCode::NotAViewDefinition,
span: None,
args,
fixes: Vec::new(),
}
}
fn validate_node(node: Node, value: &Value, pointer: &str, out: &mut Vec<Diagnostic>) {
let Some(obj) = value.as_object() else {
return;
};
let fields = node.fields();
for (key, val) in obj {
if let Some(base) = key.strip_prefix('_') {
if fields
.iter()
.any(|f| f.key == base && f.kind.is_primitive())
{
continue;
}
}
match fields.iter().find(|f| f.key == key.as_str()) {
Some(f) => check_field_value(f, val, pointer, out),
None => out.push(unknown_key(pointer, key, fields, obj)),
}
}
for f in fields.iter().filter(|f| f.required) {
if !obj.contains_key(f.key) {
out.push(missing_required(pointer, f.key));
}
}
}
fn check_field_value(f: &Field, val: &Value, parent_pointer: &str, out: &mut Vec<Diagnostic>) {
let pointer = child_pointer(parent_pointer, f.key);
let type_ok = match f.kind {
Kind::Any => true,
Kind::String => val.is_string(),
Kind::Number => val.is_number(),
Kind::Boolean => val.is_boolean(),
Kind::StringArray | Kind::ObjectArray(_) => val.is_array(),
Kind::Object(_) => val.is_object(),
};
if !type_ok {
out.push(wrong_type(&pointer, f.kind, val));
return;
}
if f.required && is_empty(f.kind, val) {
out.push(empty_required(&pointer, f.key));
return;
}
match f.kind {
Kind::Any | Kind::String | Kind::Number | Kind::Boolean => {}
Kind::StringArray => {
for (i, item) in val.as_array().into_iter().flatten().enumerate() {
if !item.is_string() {
out.push(wrong_type(&format!("{pointer}/{i}"), Kind::String, item));
}
}
}
Kind::Object(node) => validate_node(node, val, &pointer, out),
Kind::ObjectArray(node) => {
for (i, item) in val.as_array().into_iter().flatten().enumerate() {
let item_pointer = format!("{pointer}/{i}");
if item.is_object() {
validate_node(node, item, &item_pointer, out);
} else {
out.push(wrong_type(&item_pointer, Kind::Object(node), item));
}
}
}
}
}
fn is_empty(kind: Kind, val: &Value) -> bool {
match kind {
Kind::String => val.as_str().is_some_and(|s| s.trim().is_empty()),
Kind::StringArray | Kind::ObjectArray(_) => val.as_array().is_some_and(|a| a.is_empty()),
Kind::Any | Kind::Number | Kind::Boolean | Kind::Object(_) => false,
}
}
fn check_constant_value(constant: &Value, pointer: &str, out: &mut Vec<Diagnostic>) {
let Some(obj) = constant.as_object() else {
return;
};
let name = obj.get("name").and_then(Value::as_str);
let present: Vec<&str> = CONSTANT_VALUE_KEYS
.iter()
.filter(|k| obj.contains_key(**k))
.copied()
.collect();
match present.len() {
0 => out.push(constant_value_diagnostic(
pointer,
DiagnosticCode::MissingRequired,
"missing required key `value[x]`".to_string(),
"missing",
name,
)),
1 => {}
_ => out.push(constant_value_diagnostic(
pointer,
DiagnosticCode::WrongType,
format!(
"a constant may set only one value[x] key, found {}: {}",
present.len(),
present.join(", ")
),
"multiple",
name,
)),
}
}
fn constant_value_diagnostic(
pointer: &str,
code: DiagnosticCode,
message: String,
variant: &'static str,
name: Option<&str>,
) -> Diagnostic {
let mut args = BTreeMap::new();
args.insert("variant".to_string(), variant.to_string());
if let Some(name) = name {
args.insert("name".to_string(), name.to_string());
}
Diagnostic {
pointer: pointer.to_string(),
message,
severity: Severity::Error,
code,
span: None,
args,
fixes: Vec::new(),
}
}
fn damerau_levenshtein_distance(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let (rows, cols) = (a.len() + 1, b.len() + 1);
let mut d = vec![vec![0usize; cols]; rows];
for (i, row) in d.iter_mut().enumerate() {
row[0] = i;
}
for (j, cell) in d[0].iter_mut().enumerate() {
*cell = j;
}
for i in 1..rows {
for j in 1..cols {
let cost = usize::from(a[i - 1] != b[j - 1]);
d[i][j] = (d[i - 1][j] + 1)
.min(d[i][j - 1] + 1)
.min(d[i - 1][j - 1] + cost);
if i > 1 && j > 1 && a[i - 1] == b[j - 2] && a[i - 2] == b[j - 1] {
d[i][j] = d[i][j].min(d[i - 2][j - 2] + 1);
}
}
}
d[rows - 1][cols - 1]
}
fn is_singular_plural_variant(a: &str, b: &str) -> bool {
a.strip_suffix('s') == Some(b) || b.strip_suffix('s') == Some(a)
}
fn suggest_key<'a>(
key: &str,
fields: &'a [Field],
existing: &serde_json::Map<String, Value>,
) -> Option<&'a str> {
let key_lower = key.to_ascii_lowercase();
fields
.iter()
.enumerate()
.filter(|(_, f)| !existing.contains_key(f.key))
.filter_map(|(index, f)| {
let candidate_lower = f.key.to_ascii_lowercase();
let distance = damerau_levenshtein_distance(&key_lower, &candidate_lower);
let qualifies =
distance <= 2 || is_singular_plural_variant(&key_lower, &candidate_lower);
qualifies.then_some((distance, index, f.key))
})
.min_by_key(|&(distance, index, _)| (distance, index))
.map(|(_, _, key)| key)
}
fn unknown_key(
pointer: &str,
key: &str,
fields: &[Field],
present: &serde_json::Map<String, Value>,
) -> Diagnostic {
let key_pointer = child_pointer(pointer, key);
let mut args = BTreeMap::new();
args.insert("key".to_string(), key.to_string());
let mut fixes = Vec::new();
if let Some(suggestion) = suggest_key(key, fields, present) {
args.insert("suggestion".to_string(), suggestion.to_string());
fixes.push(Fix::RenameKey {
pointer: key_pointer.clone(),
to: suggestion.to_string(),
});
}
fixes.push(Fix::RemoveKey {
pointer: key_pointer.clone(),
});
Diagnostic {
pointer: key_pointer,
message: format!("unknown key `{key}`"),
severity: Severity::Error,
code: DiagnosticCode::UnknownKey,
span: None,
args,
fixes,
}
}
fn missing_required(pointer: &str, key: &str) -> Diagnostic {
let mut args = BTreeMap::new();
args.insert("key".to_string(), key.to_string());
Diagnostic {
pointer: pointer.to_string(),
message: format!("missing required key `{key}`"),
severity: Severity::Error,
code: DiagnosticCode::MissingRequired,
span: None,
args,
fixes: Vec::new(),
}
}
fn empty_required(pointer: &str, key: &str) -> Diagnostic {
let mut args = BTreeMap::new();
args.insert("key".to_string(), key.to_string());
Diagnostic {
pointer: pointer.to_string(),
message: "required value must not be empty".to_string(),
severity: Severity::Error,
code: DiagnosticCode::EmptyRequired,
span: None,
args,
fixes: Vec::new(),
}
}
fn unknown_resource_type(found: &str) -> Diagnostic {
let mut args = BTreeMap::new();
args.insert("found".to_string(), found.to_string());
Diagnostic {
pointer: "/resource".to_string(),
message: format!("unknown resource type {found:?}"),
severity: Severity::Error,
code: DiagnosticCode::UnknownResourceType,
span: None,
args,
fixes: Vec::new(),
}
}
fn wrong_type(pointer: &str, expected: Kind, found: &Value) -> Diagnostic {
let expected = match expected {
Kind::String => "a string",
Kind::Number => "a number",
Kind::Boolean => "a boolean",
Kind::StringArray | Kind::ObjectArray(_) => "an array",
Kind::Object(_) => "an object",
Kind::Any => "any value",
};
let found = type_name(found);
let mut args = BTreeMap::new();
args.insert("expected".to_string(), expected.to_string());
args.insert("found".to_string(), found.to_string());
Diagnostic {
pointer: pointer.to_string(),
message: format!("expected {expected}, found {found}"),
severity: Severity::Error,
code: DiagnosticCode::WrongType,
span: None,
args,
fixes: Vec::new(),
}
}
fn has_output_content(obj: &serde_json::Map<String, Value>, key: &str) -> bool {
obj.get(key)
.and_then(Value::as_array)
.is_some_and(|a| !a.is_empty())
}
fn check_select_shape(select: &Value, pointer: &str, out: &mut Vec<Diagnostic>) {
let Some(obj) = select.as_object() else {
return;
};
if !has_output_content(obj, "column")
&& !has_output_content(obj, "select")
&& !has_output_content(obj, "unionAll")
{
out.push(Diagnostic {
pointer: pointer.to_string(),
message: "a select must have at least one of column, select, or unionAll".to_string(),
severity: Severity::Error,
code: DiagnosticCode::SelectWithoutOutput,
span: None,
args: BTreeMap::new(),
fixes: Vec::new(),
});
}
const ITERATION_DIRECTIVES: [&str; 3] = ["forEach", "forEachOrNull", "repeat"];
let directive_count = ITERATION_DIRECTIVES
.iter()
.filter(|k| obj.contains_key(**k))
.count();
if directive_count > 1 {
out.push(multiple_iteration_directives(
pointer,
obj,
&ITERATION_DIRECTIVES,
));
}
if let Some(nested) = obj.get("select").and_then(Value::as_array) {
for (i, child) in nested.iter().enumerate() {
check_select_shape(child, &format!("{pointer}/select/{i}"), out);
}
}
if let Some(branches) = obj.get("unionAll").and_then(Value::as_array) {
for (i, branch) in branches.iter().enumerate() {
check_select_shape(branch, &format!("{pointer}/unionAll/{i}"), out);
}
}
}
fn multiple_iteration_directives(
pointer: &str,
obj: &serde_json::Map<String, Value>,
directives: &[&str],
) -> Diagnostic {
let present: Vec<&str> = directives
.iter()
.copied()
.filter(|key| obj.contains_key(*key))
.collect();
let mut args = BTreeMap::new();
args.insert("keys".to_string(), present.join(", "));
let fixes = present
.iter()
.skip(1)
.map(|key| Fix::RemoveKey {
pointer: child_pointer(pointer, key),
})
.collect();
Diagnostic {
pointer: pointer.to_string(),
message: "a select may set at most one of forEach, forEachOrNull, repeat".to_string(),
severity: Severity::Error,
code: DiagnosticCode::MultipleIterationDirectives,
span: None,
args,
fixes,
}
}
fn collect_column_names(select: &Value, out: &mut HashSet<String>) {
let Some(obj) = select.as_object() else {
return;
};
if let Some(columns) = obj.get("column").and_then(Value::as_array) {
for column in columns {
if let Some(name) = column.get("name").and_then(Value::as_str) {
out.insert(name.to_string());
}
}
}
if let Some(nested) = obj.get("select").and_then(Value::as_array) {
for child in nested {
collect_column_names(child, out);
}
}
if let Some(branches) = obj.get("unionAll").and_then(Value::as_array) {
for branch in branches {
collect_column_names(branch, out);
}
}
}
fn duplicate_column_name(
name_pointer: &str,
name: &str,
used_names: &mut HashSet<String>,
) -> Diagnostic {
let mut suffix = 2;
let mut candidate = format!("{name}_{suffix}");
while used_names.contains(&candidate) {
suffix += 1;
candidate = format!("{name}_{suffix}");
}
used_names.insert(candidate.clone());
let mut args = BTreeMap::new();
args.insert("name".to_string(), name.to_string());
Diagnostic {
pointer: name_pointer.to_string(),
message: format!("duplicate column name `{name}`"),
severity: Severity::Error,
code: DiagnosticCode::DuplicateColumnName,
span: None,
args,
fixes: vec![Fix::SetString {
pointer: name_pointer.to_string(),
value: candidate,
}],
}
}
fn check_duplicate_columns(
select: &Value,
pointer: &str,
mut scope: Vec<(String, String)>,
used_names: &mut HashSet<String>,
out: &mut Vec<Diagnostic>,
) -> Vec<(String, String)> {
let Some(obj) = select.as_object() else {
return scope;
};
if let Some(columns) = obj.get("column").and_then(Value::as_array) {
for (i, column) in columns.iter().enumerate() {
let Some(name) = column.get("name").and_then(Value::as_str) else {
continue;
};
let name_pointer = format!("{pointer}/column/{i}/name");
if scope.iter().any(|(seen, _)| seen == name) {
out.push(duplicate_column_name(&name_pointer, name, used_names));
}
scope.push((name.to_string(), name_pointer));
}
}
if let Some(nested) = obj.get("select").and_then(Value::as_array) {
for (i, child) in nested.iter().enumerate() {
scope = check_duplicate_columns(
child,
&format!("{pointer}/select/{i}"),
scope,
used_names,
out,
);
}
}
if let Some(branches) = obj.get("unionAll").and_then(Value::as_array) {
for (i, branch) in branches.iter().enumerate() {
check_duplicate_columns(
branch,
&format!("{pointer}/unionAll/{i}"),
scope.clone(),
used_names,
out,
);
}
}
scope
}
fn check_fhirpath_in_select(
select: &Value,
pointer: &str,
declared_constants: &HashSet<&str>,
out: &mut Vec<Diagnostic>,
) {
let Some(obj) = select.as_object() else {
return;
};
if let Some(columns) = obj.get("column").and_then(Value::as_array) {
for (i, column) in columns.iter().enumerate() {
if let Some(path) = column.get("path").and_then(Value::as_str) {
check_expression(
path,
&format!("{pointer}/column/{i}/path"),
declared_constants,
out,
);
}
}
}
for key in ["forEach", "forEachOrNull"] {
if let Some(expr) = obj.get(key).and_then(Value::as_str) {
check_expression(expr, &format!("{pointer}/{key}"), declared_constants, out);
}
}
if let Some(items) = obj.get("repeat").and_then(Value::as_array) {
for (i, item) in items.iter().enumerate() {
if let Some(expr) = item.as_str() {
check_expression(
expr,
&format!("{pointer}/repeat/{i}"),
declared_constants,
out,
);
}
}
}
if let Some(nested) = obj.get("select").and_then(Value::as_array) {
for (i, child) in nested.iter().enumerate() {
check_fhirpath_in_select(
child,
&format!("{pointer}/select/{i}"),
declared_constants,
out,
);
}
}
if let Some(branches) = obj.get("unionAll").and_then(Value::as_array) {
for (i, branch) in branches.iter().enumerate() {
check_fhirpath_in_select(
branch,
&format!("{pointer}/unionAll/{i}"),
declared_constants,
out,
);
}
}
}
fn check_expression(
expression: &str,
pointer: &str,
declared_constants: &HashSet<&str>,
out: &mut Vec<Diagnostic>,
) {
if expression.trim().is_empty() {
let message = "empty expression".to_string();
let mut args = BTreeMap::new();
args.insert("detail".to_string(), message.clone());
out.push(Diagnostic {
pointer: pointer.to_string(),
message,
severity: Severity::Error,
code: DiagnosticCode::FhirPathSyntax,
span: Some(Span { start: 0, end: 0 }),
args,
fixes: Vec::new(),
});
return;
}
match helios_fhirpath::parse_expression_spanned(expression) {
Err(errors) => {
if let Some(first) = errors.into_iter().next() {
let mut args = BTreeMap::new();
args.insert("detail".to_string(), first.message.clone());
out.push(Diagnostic {
pointer: pointer.to_string(),
message: first.message,
severity: Severity::Error,
code: DiagnosticCode::FhirPathSyntax,
span: Some(Span {
start: first.span.0,
end: first.span.1,
}),
args,
fixes: Vec::new(),
});
}
}
Ok(parsed) => {
for constant_ref in helios_fhirpath::external_constants(&parsed, expression) {
if declared_constants.contains(constant_ref.name.as_str())
|| helios_fhirpath::is_environment_variable(&constant_ref.name)
|| SQL_ON_FHIR_ENVIRONMENT_VARIABLES.contains(&constant_ref.name.as_str())
{
continue;
}
let (start, end) =
helios_fhirpath::expr_span_to_char_offsets(expression, &constant_ref.span);
let mut args = BTreeMap::new();
args.insert("name".to_string(), constant_ref.name.clone());
out.push(Diagnostic {
pointer: pointer.to_string(),
message: format!("undeclared constant `%{}`", constant_ref.name),
severity: Severity::Error,
code: DiagnosticCode::UndeclaredConstant,
span: Some(Span { start, end }),
args,
fixes: Vec::new(),
});
}
}
}
}
fn child_pointer(pointer: &str, key: &str) -> String {
if key.contains('~') || key.contains('/') {
format!("{pointer}/{}", key.replace('~', "~0").replace('/', "~1"))
} else {
format!("{pointer}/{key}")
}
}
fn unescape_pointer_segment(segment: &str) -> std::borrow::Cow<'_, str> {
if segment.contains('~') {
std::borrow::Cow::Owned(segment.replace("~1", "/").replace("~0", "~"))
} else {
std::borrow::Cow::Borrowed(segment)
}
}
fn document_position(root: &Value, pointer: &str) -> Vec<usize> {
let mut position = Vec::new();
let mut node = root;
for raw_segment in pointer.split('/').skip(1) {
let segment = unescape_pointer_segment(raw_segment);
match node {
Value::Object(map) => match map.keys().position(|k| k == segment.as_ref()) {
Some(index) => {
position.push(index);
node = map.get(segment.as_ref()).expect("just located by key");
}
None => break,
},
Value::Array(items) => match segment.parse::<usize>() {
Ok(index) if index < items.len() => {
position.push(index);
node = &items[index];
}
_ => break,
},
_ => break,
}
}
position
}
#[cfg(test)]
mod tests;