use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::types::{DatePrecision, SearchParamType};
use super::errors::ExtractionError;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum IndexValue {
String(String),
Token {
system: Option<String>,
code: String,
display: Option<String>,
identifier_type_system: Option<String>,
identifier_type_code: Option<String>,
},
Date {
value: String,
precision: DatePrecision,
},
Number(f64),
Quantity {
value: f64,
unit: Option<String>,
system: Option<String>,
code: Option<String>,
},
Reference {
reference: String,
resource_type: Option<String>,
resource_id: Option<String>,
},
Uri(String),
}
impl IndexValue {
pub fn string(s: impl Into<String>) -> Self {
IndexValue::String(s.into())
}
pub fn token(system: Option<String>, code: impl Into<String>) -> Self {
IndexValue::Token {
system,
code: code.into(),
display: None,
identifier_type_system: None,
identifier_type_code: None,
}
}
pub fn token_code(code: impl Into<String>) -> Self {
IndexValue::Token {
system: None,
code: code.into(),
display: None,
identifier_type_system: None,
identifier_type_code: None,
}
}
pub fn token_with_display(
system: Option<String>,
code: impl Into<String>,
display: Option<String>,
) -> Self {
IndexValue::Token {
system,
code: code.into(),
display,
identifier_type_system: None,
identifier_type_code: None,
}
}
pub fn identifier_with_type(
system: Option<String>,
value: impl Into<String>,
type_system: Option<String>,
type_code: Option<String>,
) -> Self {
IndexValue::Token {
system,
code: value.into(),
display: None,
identifier_type_system: type_system,
identifier_type_code: type_code,
}
}
pub fn token_display_only(display: impl Into<String>) -> Self {
IndexValue::Token {
system: None,
code: String::new(), display: Some(display.into()),
identifier_type_system: None,
identifier_type_code: None,
}
}
pub fn date(value: impl Into<String>) -> Self {
let value = value.into();
let precision = DatePrecision::from_date_string(&value);
IndexValue::Date { value, precision }
}
pub fn number(value: f64) -> Self {
IndexValue::Number(value)
}
pub fn quantity(value: f64, unit: Option<String>, system: Option<String>) -> Self {
IndexValue::Quantity {
value,
unit: unit.clone(),
system,
code: unit,
}
}
pub fn reference(reference: impl Into<String>) -> Self {
let reference = reference.into();
let (resource_type, resource_id) = parse_reference(&reference);
IndexValue::Reference {
reference,
resource_type,
resource_id,
}
}
pub fn uri(uri: impl Into<String>) -> Self {
IndexValue::Uri(uri.into())
}
pub fn as_string(&self) -> Option<&str> {
match self {
IndexValue::String(s) => Some(s),
_ => None,
}
}
pub fn param_type(&self) -> SearchParamType {
match self {
IndexValue::String(_) => SearchParamType::String,
IndexValue::Token { .. } => SearchParamType::Token,
IndexValue::Date { .. } => SearchParamType::Date,
IndexValue::Number(_) => SearchParamType::Number,
IndexValue::Quantity { .. } => SearchParamType::Quantity,
IndexValue::Reference { .. } => SearchParamType::Reference,
IndexValue::Uri(_) => SearchParamType::Uri,
}
}
}
fn parse_reference(reference: &str) -> (Option<String>, Option<String>) {
if reference.starts_with("http://") || reference.starts_with("https://") {
let parts: Vec<&str> = reference.rsplitn(3, '/').collect();
if parts.len() >= 2 {
return (Some(parts[1].to_string()), Some(parts[0].to_string()));
}
}
let parts: Vec<&str> = reference.split('/').collect();
if parts.len() == 2 {
return (Some(parts[0].to_string()), Some(parts[1].to_string()));
}
(None, None)
}
pub struct ValueConverter;
impl ValueConverter {
pub fn convert(
value: &Value,
target_type: SearchParamType,
param_name: &str,
) -> Result<Vec<IndexValue>, ExtractionError> {
match value {
Value::Array(arr) => {
let mut results = Vec::new();
for item in arr {
results.extend(Self::convert_single(item, target_type, param_name)?);
}
Ok(results)
}
_ => Self::convert_single(value, target_type, param_name),
}
}
fn convert_single(
value: &Value,
target_type: SearchParamType,
param_name: &str,
) -> Result<Vec<IndexValue>, ExtractionError> {
match target_type {
SearchParamType::String => Self::convert_to_string(value, param_name),
SearchParamType::Token => Self::convert_to_token(value, param_name),
SearchParamType::Date => Self::convert_to_date(value, param_name),
SearchParamType::Number => Self::convert_to_number(value, param_name),
SearchParamType::Quantity => Self::convert_to_quantity(value, param_name),
SearchParamType::Reference => Self::convert_to_reference(value, param_name),
SearchParamType::Uri => Self::convert_to_uri(value, param_name),
SearchParamType::Composite => {
Ok(Vec::new())
}
SearchParamType::Special => {
Self::convert_special(value, param_name)
}
}
}
fn convert_to_string(
value: &Value,
_param_name: &str,
) -> Result<Vec<IndexValue>, ExtractionError> {
let mut results = Vec::new();
match value {
Value::String(s) => {
results.push(IndexValue::string(s.to_lowercase()));
}
Value::Object(obj) => {
if let Some(family) = obj.get("family").and_then(|v| v.as_str()) {
results.push(IndexValue::string(family.to_lowercase()));
}
if let Some(given) = obj.get("given").and_then(|v| v.as_array()) {
for g in given {
if let Some(s) = g.as_str() {
results.push(IndexValue::string(s.to_lowercase()));
}
}
}
if let Some(text) = obj.get("text").and_then(|v| v.as_str()) {
results.push(IndexValue::string(text.to_lowercase()));
}
if let Some(line) = obj.get("line").and_then(|v| v.as_array()) {
for l in line {
if let Some(s) = l.as_str() {
results.push(IndexValue::string(s.to_lowercase()));
}
}
}
if let Some(city) = obj.get("city").and_then(|v| v.as_str()) {
results.push(IndexValue::string(city.to_lowercase()));
}
if let Some(state) = obj.get("state").and_then(|v| v.as_str()) {
results.push(IndexValue::string(state.to_lowercase()));
}
if let Some(postal) = obj.get("postalCode").and_then(|v| v.as_str()) {
results.push(IndexValue::string(postal.to_lowercase()));
}
if let Some(country) = obj.get("country").and_then(|v| v.as_str()) {
results.push(IndexValue::string(country.to_lowercase()));
}
}
_ => {}
}
Ok(results)
}
fn convert_to_token(
value: &Value,
_param_name: &str,
) -> Result<Vec<IndexValue>, ExtractionError> {
let mut results = Vec::new();
match value {
Value::String(s) => {
results.push(IndexValue::token_code(s.clone()));
}
Value::Bool(b) => {
results.push(IndexValue::token_code(b.to_string()));
}
Value::Object(obj) => {
if obj.contains_key("code") && !obj.contains_key("coding") {
let system = obj.get("system").and_then(|v| v.as_str()).map(String::from);
let code = obj.get("code").and_then(|v| v.as_str()).unwrap_or_default();
let display = obj
.get("display")
.and_then(|v| v.as_str())
.map(String::from);
if !code.is_empty() {
results.push(IndexValue::token_with_display(system, code, display));
}
}
if let Some(coding) = obj.get("coding").and_then(|v| v.as_array()) {
for c in coding {
if let Some(code) = c.get("code").and_then(|v| v.as_str()) {
let system = c.get("system").and_then(|v| v.as_str()).map(String::from);
let display =
c.get("display").and_then(|v| v.as_str()).map(String::from);
results.push(IndexValue::token_with_display(system, code, display));
}
}
if let Some(text) = obj.get("text").and_then(|v| v.as_str()) {
if !text.is_empty() {
results.push(IndexValue::token_display_only(text));
}
}
}
if obj.contains_key("value")
&& !obj.contains_key("code")
&& !obj.contains_key("coding")
{
let system = obj.get("system").and_then(|v| v.as_str()).map(String::from);
let value = obj
.get("value")
.and_then(|v| v.as_str())
.unwrap_or_default();
let (type_system, type_code) = obj
.get("type")
.and_then(|t| t.get("coding"))
.and_then(|c| c.as_array())
.and_then(|arr| arr.first())
.map(|coding| {
(
coding
.get("system")
.and_then(|v| v.as_str())
.map(String::from),
coding
.get("code")
.and_then(|v| v.as_str())
.map(String::from),
)
})
.unwrap_or((None, None));
if !value.is_empty() {
results.push(IndexValue::identifier_with_type(
system,
value,
type_system,
type_code,
));
}
}
if let Some(val) = obj.get("value").and_then(|v| v.as_str()) {
if obj.contains_key("system")
&& obj
.get("system")
.and_then(|v| v.as_str())
.map(|s| s == "phone" || s == "email")
.unwrap_or(false)
{
let system_type =
obj.get("system").and_then(|v| v.as_str()).map(String::from);
results.push(IndexValue::token(system_type, val));
}
}
}
_ => {}
}
Ok(results)
}
fn convert_to_date(
value: &Value,
_param_name: &str,
) -> Result<Vec<IndexValue>, ExtractionError> {
let mut results = Vec::new();
match value {
Value::String(s) => {
results.push(IndexValue::date(s.clone()));
}
Value::Object(obj) => {
if let Some(start) = obj.get("start").and_then(|v| v.as_str()) {
results.push(IndexValue::date(start));
}
if let Some(end) = obj.get("end").and_then(|v| v.as_str()) {
results.push(IndexValue::date(end));
}
if let Some(repeat) = obj.get("repeat").and_then(|v| v.as_object()) {
if let Some(bounds_period) =
repeat.get("boundsPeriod").and_then(|v| v.as_object())
{
if let Some(start) = bounds_period.get("start").and_then(|v| v.as_str()) {
results.push(IndexValue::date(start));
}
}
}
}
_ => {}
}
Ok(results)
}
fn convert_to_number(
value: &Value,
param_name: &str,
) -> Result<Vec<IndexValue>, ExtractionError> {
match value {
Value::Number(n) => {
let f = n
.as_f64()
.ok_or_else(|| ExtractionError::ConversionFailed {
param_name: param_name.to_string(),
expected_type: "number".to_string(),
actual_value: n.to_string(),
})?;
Ok(vec![IndexValue::number(f)])
}
Value::String(s) => {
let f: f64 = s.parse().map_err(|_| ExtractionError::ConversionFailed {
param_name: param_name.to_string(),
expected_type: "number".to_string(),
actual_value: s.clone(),
})?;
Ok(vec![IndexValue::number(f)])
}
_ => Ok(Vec::new()),
}
}
fn convert_to_quantity(
value: &Value,
_param_name: &str,
) -> Result<Vec<IndexValue>, ExtractionError> {
let mut results = Vec::new();
if let Value::Object(obj) = value {
if let Some(val) = obj.get("value").and_then(|v| v.as_f64()) {
let unit = obj.get("unit").and_then(|v| v.as_str()).map(String::from);
let system = obj.get("system").and_then(|v| v.as_str()).map(String::from);
let code = obj.get("code").and_then(|v| v.as_str()).map(String::from);
results.push(IndexValue::Quantity {
value: val,
unit: unit.or_else(|| code.clone()),
system,
code,
});
}
}
Ok(results)
}
fn convert_to_reference(
value: &Value,
_param_name: &str,
) -> Result<Vec<IndexValue>, ExtractionError> {
let mut results = Vec::new();
match value {
Value::String(s) => {
results.push(IndexValue::reference(s.clone()));
}
Value::Object(obj) => {
if let Some(reference) = obj.get("reference").and_then(|v| v.as_str()) {
results.push(IndexValue::reference(reference));
}
}
_ => {}
}
Ok(results)
}
fn convert_to_uri(
value: &Value,
_param_name: &str,
) -> Result<Vec<IndexValue>, ExtractionError> {
match value {
Value::String(s) => Ok(vec![IndexValue::uri(s.clone())]),
_ => Ok(Vec::new()),
}
}
fn convert_special(
value: &Value,
param_name: &str,
) -> Result<Vec<IndexValue>, ExtractionError> {
match param_name {
"_id" => {
if let Value::String(s) = value {
Ok(vec![IndexValue::token_code(s.clone())])
} else {
Ok(Vec::new())
}
}
"_lastUpdated" => Self::convert_to_date(value, param_name),
"_tag" | "_security" => Self::convert_to_token(value, param_name),
"_profile" | "_source" => Self::convert_to_uri(value, param_name),
_ => Ok(Vec::new()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_index_value_creation() {
let s = IndexValue::string("test");
assert_eq!(s.as_string(), Some("test"));
assert_eq!(s.param_type(), SearchParamType::String);
let t = IndexValue::token(Some("http://loinc.org".to_string()), "1234-5");
assert_eq!(t.param_type(), SearchParamType::Token);
let d = IndexValue::date("2024-01-15");
if let IndexValue::Date { precision, .. } = d {
assert_eq!(precision, DatePrecision::Day);
}
let r = IndexValue::reference("Patient/123");
if let IndexValue::Reference {
resource_type,
resource_id,
..
} = r
{
assert_eq!(resource_type, Some("Patient".to_string()));
assert_eq!(resource_id, Some("123".to_string()));
}
}
#[test]
fn test_parse_reference() {
let (rt, id) = parse_reference("Patient/123");
assert_eq!(rt, Some("Patient".to_string()));
assert_eq!(id, Some("123".to_string()));
let (rt, id) = parse_reference("http://example.com/fhir/Patient/456");
assert_eq!(rt, Some("Patient".to_string()));
assert_eq!(id, Some("456".to_string()));
}
#[test]
fn test_convert_string() {
let value = json!("Smith");
let results = ValueConverter::convert(&value, SearchParamType::String, "name").unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].as_string(), Some("smith")); }
#[test]
fn test_convert_human_name() {
let value = json!({
"family": "Smith",
"given": ["John", "Jane"]
});
let results = ValueConverter::convert(&value, SearchParamType::String, "name").unwrap();
assert_eq!(results.len(), 3); }
#[test]
fn test_convert_token_coding() {
let value = json!({
"system": "http://loinc.org",
"code": "12345-6"
});
let results = ValueConverter::convert(&value, SearchParamType::Token, "code").unwrap();
assert_eq!(results.len(), 1);
if let IndexValue::Token { system, code, .. } = &results[0] {
assert_eq!(system.as_ref().unwrap(), "http://loinc.org");
assert_eq!(code, "12345-6");
}
}
#[test]
fn test_convert_codeable_concept() {
let value = json!({
"coding": [
{"system": "http://snomed.info/sct", "code": "123"},
{"system": "http://icd10.info", "code": "456"}
],
"text": "Some condition"
});
let results = ValueConverter::convert(&value, SearchParamType::Token, "code").unwrap();
assert_eq!(results.len(), 3);
}
#[test]
fn test_convert_identifier() {
let value = json!({
"system": "http://hospital.org/mrn",
"value": "12345"
});
let results =
ValueConverter::convert(&value, SearchParamType::Token, "identifier").unwrap();
assert_eq!(results.len(), 1);
if let IndexValue::Token { system, code, .. } = &results[0] {
assert_eq!(system.as_ref().unwrap(), "http://hospital.org/mrn");
assert_eq!(code, "12345");
}
}
#[test]
fn test_convert_date() {
let value = json!("2024-01-15T10:30:00Z");
let results = ValueConverter::convert(&value, SearchParamType::Date, "date").unwrap();
assert_eq!(results.len(), 1);
if let IndexValue::Date { value, precision } = &results[0] {
assert!(value.starts_with("2024-01-15"));
assert_eq!(*precision, DatePrecision::Second);
}
}
#[test]
fn test_convert_period() {
let value = json!({
"start": "2024-01-01",
"end": "2024-01-31"
});
let results = ValueConverter::convert(&value, SearchParamType::Date, "date").unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn test_convert_quantity() {
let value = json!({
"value": 120.5,
"unit": "mmHg",
"system": "http://unitsofmeasure.org",
"code": "mm[Hg]"
});
let results =
ValueConverter::convert(&value, SearchParamType::Quantity, "value-quantity").unwrap();
assert_eq!(results.len(), 1);
if let IndexValue::Quantity {
value,
unit,
system,
code,
} = &results[0]
{
assert!((value - 120.5).abs() < f64::EPSILON);
assert_eq!(unit.as_ref().unwrap(), "mmHg");
assert_eq!(system.as_ref().unwrap(), "http://unitsofmeasure.org");
assert_eq!(code.as_ref().unwrap(), "mm[Hg]");
}
}
#[test]
fn test_convert_reference_object() {
let value = json!({
"reference": "Patient/123"
});
let results =
ValueConverter::convert(&value, SearchParamType::Reference, "subject").unwrap();
assert_eq!(results.len(), 1);
if let IndexValue::Reference {
reference,
resource_type,
resource_id,
} = &results[0]
{
assert_eq!(reference, "Patient/123");
assert_eq!(resource_type.as_ref().unwrap(), "Patient");
assert_eq!(resource_id.as_ref().unwrap(), "123");
}
}
#[test]
fn test_convert_array() {
let value = json!(["one", "two", "three"]);
let results = ValueConverter::convert(&value, SearchParamType::String, "name").unwrap();
assert_eq!(results.len(), 3);
}
#[test]
fn test_convert_codeable_concept_with_display() {
let value = json!({
"coding": [
{
"system": "http://loinc.org",
"code": "8867-4",
"display": "Heart rate"
}
]
});
let results = ValueConverter::convert(&value, SearchParamType::Token, "code").unwrap();
assert!(!results.is_empty(), "Should have at least one result");
let heart_rate = results
.iter()
.find(|v| matches!(v, IndexValue::Token { code, .. } if code == "8867-4"));
assert!(heart_rate.is_some(), "Should have token with code 8867-4");
if let Some(IndexValue::Token {
system,
code,
display,
..
}) = heart_rate
{
assert_eq!(system.as_ref().unwrap(), "http://loinc.org");
assert_eq!(code, "8867-4");
assert_eq!(
display.as_ref().unwrap(),
"Heart rate",
"Display text should be populated"
);
}
}
#[test]
fn test_convert_identifier_with_type() {
let value = json!({
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR"
}
]
},
"system": "http://hospital.org/mrn",
"value": "MRN12345"
});
let results =
ValueConverter::convert(&value, SearchParamType::Token, "identifier").unwrap();
assert_eq!(results.len(), 1);
if let IndexValue::Token {
system,
code,
identifier_type_system,
identifier_type_code,
..
} = &results[0]
{
assert_eq!(system.as_ref().unwrap(), "http://hospital.org/mrn");
assert_eq!(code, "MRN12345");
assert_eq!(
identifier_type_system.as_ref().unwrap(),
"http://terminology.hl7.org/CodeSystem/v2-0203",
"Identifier type system should be populated"
);
assert_eq!(
identifier_type_code.as_ref().unwrap(),
"MR",
"Identifier type code should be populated"
);
} else {
panic!("Expected Token variant");
}
}
}