FHIR
HL7 FHIR R4 JSON: resources checked for required fields, bound codes and date formats, Bundles read and built, references resolved across a Bundle, FHIRPath-lite field access, and search parameters with paging, sorting and includes for a FHIR server.
Version 1.0.0 · 12 functions · 1 crate
Functions
What use fhir.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use fhir_check, fhir_outcome from fhir (Quillo 0.6).
fhir_check(resource: any) → anyChecks a resource against FHIR R4's rules for it: required elements, bound codes, id, date and dateTime formats, no empty strings, arrays or nulls, the Bundle rules, and what is contained. { ok, problems }, each problem naming its path. Not a full profile validator: the resources most in use are covered, and of those their commonest bindings.
fhir_outcome(severity: str, code: str, diagnostics: str) → anyAn OperationOutcome, what a FHIR server answers a failure with: severity (fatal, error, warning, information), code (invalid, not-found, conflict...), and the words.
fhir_bundle_read(bundle: any) → anyor an errorReads a Bundle: { type, total, links, entries: [{ full_url, resource_type, id, resource, search_mode, request, response }] }.
fhir_bundle_build(kind: str, resources: list, base: str) → anyor an errorMakes a Bundle of kind (collection, transaction, batch, document, searchset...) from resources. For a transaction or batch each entry gets its request: PUT by id, POST without one. base makes each fullUrl; entries without an id get a urn:uuid: one, and a reference written #n-free as Patient/temp keeps working only if you use those fullUrls yourself.
fhir_resolve(bundle: any, reference: any, from: any) → any?Finds what a reference points to. reference is a text or a Reference map; relative (Patient/1), absolute (https://x/fhir/Patient/1), urn:uuid: or Patient/1/_history/2. Looked for in the Bundle by fullUrl, then by type and id. #id is looked for in from's contained resources. Null when not there.
fhir_references(resource: any) → listEvery reference in a resource: [{ path, reference, display }].
fhir_path(resource: any, path: str) → listor an errorA small FHIRPath: every value at path, as a list. name.family, Patient.name[0].given, telecom.where(system='phone').value, value for a choice like valueQuantity, and first(), last(), count(), exists(), empty().
fhir_get(resource: any, path: str) → anyor an errorThe first value at path, or null.
fhir_search_parse(query: str) → anyor an errorReads a FHIR search's query text: { params: [{ name, modifier, values: [{ prefix, value }] }], count, offset, sort, include, revinclude, summary, elements }. birthdate=ge1990-01-01, name:exact=Ada, code=http://loinc.org|1234-5, a comma being "or" and a repeated parameter "and".
fhir_search(resource_type: str, resources: list, query: str, base: str) → anyor an errorRuns a search over resources as a FHIR server would, and gives a searchset Bundle: query as in fhir_search_parse, resource_type the kind searched, base the server's address. _count (default 20, at most 500) and _offset page it, with self, first, previous, next and last links. _sort by a field, - for descending; _include brings referenced resources, as entries with mode include. An unknown parameter is an error, as Prefer: handling=strict asks.
fhir_query_text(query: map) → strThe query text of a request's parsed query, for fhir_search: fhir_query_text(request.query). A map holds each parameter once.
fhir_search_url(base: str, resource_type: str, params: map) → strbase?params for a client: a map of parameters to a search address, values percent-encoded.
Get it
$ quillo install fhir # fhir.qwer into this folder$ quillo install fhir --example # and its example, fhir.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/fhir.qwer$ curl -O https://quillolang.org/plugins/files/fhir.qweThen, in your program, all of it or only what you call:
use fhir.qweruse fhir_check, fhir_outcome from fhir // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }Example
// A small FHIR R4 server and a client's reading of what it says: resources checked before they are// taken, a search with paging, a Bundle built and read, references resolved, fields read by path.// Every record here is made up.//// quillo run fhir.qwe then open http://localhost:8080/fhir/Patient?family=lov, /check, /path, /bundle, /search and /refuseduse "fhir.qwer"let BASE = "http://localhost:8080/fhir"let ADA = { resourceType: "Patient", id: "p1", gender: "female", birthDate: "1815-12-10", name: [{ use: "official", family: "Lovelace", given: ["Augusta", "Ada"] }, { use: "nickname", given: ["Ada"] }], telecom: [{ system: "phone", value: "+44 20 7946 0000" }, { system: "email", value: "ada@example.org" }], identifier: [{ system: "http://hospital.example/mrn", value: "MRN-1" }],}let STORE = [ ADA, { resourceType: "Patient", id: "p2", gender: "male", birthDate: "1791-12-26", name: [{ family: "Babbage", given: ["Charles"] }] }, { resourceType: "Patient", id: "p3", gender: "female", birthDate: "1906-12-09", name: [{ family: "Hopper", given: ["Grace"] }] }, { resourceType: "Patient", id: "p4", gender: "male", birthDate: "1912-06-23", name: [{ family: "Turing", given: ["Alan"] }] }, { resourceType: "Patient", id: "p5", gender: "female", birthDate: "1910-01-01", name: [{ family: "Lovett", given: ["Mary"] }] }, { resourceType: "Observation", id: "o1", status: "final", code: { coding: [{ system: "http://loinc.org", code: "8867-4", display: "Heart rate" }] }, subject: { reference: "Patient/p1", display: "Ada" }, effectiveDateTime: "2026-10-01T09:30:00Z", valueQuantity: { value: 72, unit: "beats/minute" } }, { resourceType: "Observation", id: "o2", status: "preliminary", code: { coding: [{ system: "http://loinc.org", code: "8310-5", display: "Body temperature" }] }, subject: { reference: "Patient/p2" }, effectiveDateTime: "2026-10-02T09:30:00+01:00", valueQuantity: { value: 36.6, unit: "Cel" } },]// The search of a FHIR server, its answer a searchset Bundle.route GET /fhir/:kind { let r = fhir_search(kind, STORE, fhir_query_text(request.query), BASE) if is_err(r) { reply 400 fhir_outcome("error", "not-supported", r.error) } r}route GET /fhir/:kind/:id { let found = find(STORE, (x) => x.resourceType == kind && x.id == id) if found == null { reply 404 fhir_outcome("error", "not-found", `${kind}/${id} is not known`) } found}// A client's check of what it means to send.route POST /fhir/:kind { let body = request.json ?? {} let checked = fhir_check(body) if !checked.ok { reply 422 fhir_outcome("error", "invalid", join(checked.problems, "; ")) } { accepted: true }}route GET /check = { good: fhir_check(ADA), observation: fhir_check({ resourceType: "Observation", code: { text: "x" } }).problems, codes: fhir_check({ resourceType: "Patient", gender: "woman", birthDate: "1815-13-10" }).problems, time_zone: fhir_check({ resourceType: "Observation", status: "final", code: { text: "x" }, effectiveDateTime: "2026-10-01T09:30:00" }).problems, choice: fhir_check({ resourceType: "MedicationRequest", status: "active", intent: "order", subject: { reference: "Patient/p1" } }).problems, empty: fhir_check({ resourceType: "Patient", name: [], gender: "" }).problems, not_one: fhir_check({ resourceType: "Patent" }).problems, org: fhir_check({ resourceType: "Organization" }).problems, bundle: fhir_check({ resourceType: "Bundle", type: "transaction", total: 1, entry: [{ resource: { resourceType: "Observation" } }] }).problems,}route GET /path = { family: fhir_get(ADA, "name.where(use='official').family")?, given: fhir_path(ADA, "Patient.name[0].given")?, phone: fhir_get(ADA, "telecom.where(system='phone').value")?, first: fhir_get(ADA, "name.given.first()")?, count: fhir_get(ADA, "telecom.count()")?, missing: fhir_get(ADA, "deceasedBoolean")?, choice: fhir_get(STORE[5], "value.value")?, unknown: fhir_path(ADA, "name.select(family)").error,}route GET /bundle { let b = fhir_bundle_build("transaction", [STORE[0], STORE[5], { resourceType: "Patient", name: [{ family: "New" }] }], BASE)? let read = fhir_bundle_read(b)? { ok: fhir_check(b).ok, requests: read.entries | map((e) => e.request), urls: read.entries | map((e) => e.full_url), subject: fhir_resolve(b, STORE[5].subject, null)?.id, by_url: fhir_resolve(b, "http://localhost:8080/fhir/Patient/p1", null)?.gender, versioned: fhir_resolve(b, "Patient/p1/_history/3", null)?.id, absent: fhir_resolve(b, "Patient/none", null), contained: fhir_resolve(b, "#m1", { contained: [{ resourceType: "Medication", id: "m1" }] })?.resourceType, refs: fhir_references(STORE[5]), wrong_kind: fhir_bundle_read(ADA).error, wrong_type: fhir_bundle_build("pile", [], "").error, }}route GET /search { let page1 = fhir_bundle_read(fhir_search("Patient", STORE, "gender=female&_sort=-birthdate&_count=2", BASE)?)? let page2 = fhir_bundle_read(fhir_search("Patient", STORE, "gender=female&_sort=-birthdate&_count=2&_offset=2", BASE)?)? { total: page1.total, first: page1.entries | map((e) => e.id), next: page1.links.next, second: page2.entries | map((e) => e.id), previous: page2.links.previous, no_next: page2.links.next, parsed: fhir_search_parse("birthdate=ge1900-01-01&name:exact=Ada&code=http://loinc.org|8867-4,8310-5&_count=5&_include=Observation:patient")?, dates: fhir_bundle_read(fhir_search("Patient", STORE, "birthdate=ge1900&birthdate=lt1911", BASE)?)?.entries | map((e) => e.id), token: fhir_bundle_read(fhir_search("Observation", STORE, "code=http://loinc.org|8867-4", BASE)?)?.entries | map((e) => e.id), identifier: fhir_bundle_read(fhir_search("Patient", STORE, "identifier=http://hospital.example/mrn|MRN-1", BASE)?)?.total, email: fhir_bundle_read(fhir_search("Patient", STORE, "email=ada@example.org", BASE)?)?.total, with_patient: fhir_bundle_read(fhir_search("Observation", STORE, "patient=p1&_include=Observation:patient", BASE)?)?.entries | map((e) => [e.search_mode, e.id]), url: fhir_search_url(BASE, "Patient", { family: "Lovelace jr", _count: "5" }), }}route GET /refused = { unknown: fhir_search("Patient", STORE, "shoe_size=9", BASE).error, modifier: fhir_search("Patient", STORE, "name:phonetic=x", BASE).error, count: fhir_search_parse("_count=many").error, outcome: fhir_outcome("error", "not-found", "Patient/x is not known"),}$ quillo run fhir.qwe$ curl 'localhost:8080/fhir/Patient?family=lov&_count=1'$ curl 'localhost:8080/check'$ curl 'localhost:8080/path'$ curl 'localhost:8080/bundle'$ curl 'localhost:8080/search'$ curl 'localhost:8080/refused'Source
//! HL7 FHIR R4 JSON: resources checked for required fields, codes and formats, Bundles read and built, references resolved, FHIRPath-lite field access, and search parameters with paging for a FHIR server.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Map, Value};/// For each kind of resource: its required elements (`x|y` for a choice, `value[x]` written as a prefix `value*`).const REQUIRED: &[(&str, &[&str])] = &[ ("AllergyIntolerance", &["patient"]), ("Appointment", &["status", "participant"]), ("Bundle", &["type"]), ("CarePlan", &["status", "intent", "subject"]), ("Claim", &["status", "type", "use", "patient", "created", "provider", "priority", "insurance"]), ("Composition", &["status", "type", "date", "author", "title"]), ("Condition", &["subject"]), ("Consent", &["status", "scope", "category"]), ("Coverage", &["status", "beneficiary", "payor"]), ("DiagnosticReport", &["status", "code"]), ("DocumentReference", &["status", "content"]), ("Encounter", &["status", "class"]), ("Immunization", &["status", "vaccineCode", "patient", "occurrence*"]), ("Location", &[]), ("Medication", &[]), ("MedicationRequest", &["status", "intent", "medication*", "subject"]), ("MedicationStatement", &["status", "medication*", "subject"]), ("Observation", &["status", "code"]), ("OperationOutcome", &["issue"]), ("Organization", &[]), ("Patient", &[]), ("Practitioner", &[]), ("PractitionerRole", &[]), ("Procedure", &["status", "subject"]), ("Questionnaire", &["status"]), ("QuestionnaireResponse", &["status"]), ("RelatedPerson", &["patient"]), ("ServiceRequest", &["status", "intent", "subject"]), ("Specimen", &[]),];/// Bound value sets of the commonest `code` elements.const CODES: &[(&str, &str, &[&str])] = &[ ("Patient", "gender", &["male", "female", "other", "unknown"]), ("Practitioner", "gender", &["male", "female", "other", "unknown"]), ("Observation", "status", &["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"]), ("DiagnosticReport", "status", &["registered", "partial", "preliminary", "final", "amended", "corrected", "appended", "cancelled", "entered-in-error", "unknown"]), ("Encounter", "status", &["planned", "arrived", "triaged", "in-progress", "onleave", "finished", "cancelled", "entered-in-error", "unknown"]), ("MedicationRequest", "status", &["active", "on-hold", "cancelled", "completed", "entered-in-error", "stopped", "draft", "unknown"]), ("MedicationRequest", "intent", &["proposal", "plan", "order", "original-order", "reflex-order", "filler-order", "instance-order", "option"]), ("Immunization", "status", &["completed", "entered-in-error", "not-done"]), ("Procedure", "status", &["preparation", "in-progress", "not-done", "on-hold", "stopped", "completed", "entered-in-error", "unknown"]), ("Appointment", "status", &["proposed", "pending", "booked", "arrived", "fulfilled", "cancelled", "noshow", "entered-in-error", "checked-in", "waitlist"]), ("Bundle", "type", &["document", "message", "transaction", "transaction-response", "batch", "batch-response", "history", "searchset", "collection"]),];const RESOURCE_TYPES: &[&str] = &["Account", "ActivityDefinition", "AdverseEvent", "Basic", "Binary", "BodyStructure", "CapabilityStatement", "CareTeam", "ChargeItem", "ClinicalImpression", "Communication", "Contract", "Device", "DeviceRequest", "EpisodeOfCare", "ExplanationOfBenefit", "FamilyMemberHistory", "Flag", "Goal", "Group", "HealthcareService", "ImagingStudy", "Invoice", "List", "Media", "MeasureReport", "MedicationAdministration", "MedicationDispense", "NutritionOrder", "Parameters", "Provenance", "RiskAssessment", "Schedule", "Slot", "StructureDefinition", "Subscription", "SupplyRequest", "Task", "ValueSet"];fn known(t: &str) -> bool { REQUIRED.iter().any(|r| r.0 == t) || RESOURCE_TYPES.contains(&t)}fn date_ok(s: &str) -> bool { let b = s.as_bytes(); let d = |r: std::ops::Range<usize>| b.get(r.clone()).is_some_and(|x| x.iter().all(u8::is_ascii_digit)); let month = |s: &str| s.get(5..7).and_then(|m| m.parse::<u32>().ok()).is_some_and(|m| (1..=12).contains(&m)); let day = |s: &str| s.get(8..10).and_then(|m| m.parse::<u32>().ok()).is_some_and(|m| (1..=31).contains(&m)); match s.len() { 4 => d(0..4), 7 => d(0..4) && b[4] == b'-' && d(5..7) && month(s), 10 => d(0..4) && b[4] == b'-' && d(5..7) && b[7] == b'-' && d(8..10) && month(s) && day(s), _ => false, }}fn datetime_ok(s: &str) -> bool { if date_ok(s) { return true; } let Some((date, time)) = s.split_once('T') else { return false }; if date.len() != 10 || !date_ok(date) { return false; } let (clock, zone) = if let Some(c) = time.strip_suffix('Z') { (c, "Z") } else if time.len() > 6 && (time.as_bytes()[time.len() - 6] == b'+' || time.as_bytes()[time.len() - 6] == b'-') { time.split_at(time.len() - 6) } else { return false; // FHIR wants a zone whenever a time is given }; let clock = clock.split('.').next().unwrap_or(""); let n = clock.as_bytes(); let hhmmss = clock.len() == 8 && n[2] == b':' && n[5] == b':' && [0, 1, 3, 4, 6, 7].iter().all(|i| n[*i].is_ascii_digit()); hhmmss && (zone == "Z" || zone[1..].len() == 5)}fn id_ok(s: &str) -> bool { !s.is_empty() && s.len() <= 64 && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.')}/// Date-like elements checked anywhere in a resource, with the way they are named.fn walk(path: &str, v: &Value, p: &mut Vec<String>) { match v { Value::Object(m) => { for (k, x) in m { let at = format!("{}.{}", path, k); if let Some(s) = x.as_str() { let name = k.to_ascii_lowercase(); if name == "birthdate" || name == "issued" || name.ends_with("datetime") || k == "effectiveDateTime" || k == "authoredOn" || k == "start" || k == "end" || k == "recordedDate" { if !datetime_ok(s) { p.push(format!("{}: `{}` is not a FHIR date or dateTime (a time needs its zone)", at, s)); } } if k == "reference" && (s.is_empty() || s.contains(' ')) { p.push(format!("{}: `{}` is not a reference", at, s)); } if s.is_empty() { p.push(format!("{}: strings may not be empty in FHIR: leave the element out", at)); } } if x.is_null() { p.push(format!("{}: FHIR has no null outside of arrays lined up with primitive extensions", at)); } if k == "contained" { for (i, c) in x.as_array().into_iter().flatten().enumerate() { check_one(c, &format!("{}[{}]", at, i), p); } continue; } walk(&at, x, p); } } Value::Array(a) => { if a.is_empty() { p.push(format!("{}: arrays may not be empty in FHIR: leave the element out", path)); } for (i, x) in a.iter().enumerate() { walk(&format!("{}[{}]", path, i), x, p); } } _ => {} }}fn check_one(r: &Value, at: &str, p: &mut Vec<String>) { let Some(t) = r.get("resourceType").and_then(Value::as_str) else { p.push(format!("{}: no resourceType", at)); return; }; let at = if at.is_empty() { t.to_string() } else { format!("{}({})", at, t) }; if !known(t) { p.push(format!("{}: `{}` is not a FHIR R4 resource this knows", at, t)); return; } if let Some(id) = r.get("id") { if !id.as_str().is_some_and(id_ok) { p.push(format!("{}.id: {} should be 1 to 64 letters, digits, dashes or dots", at, id)); } } if let Some((_, need)) = REQUIRED.iter().find(|x| x.0 == t) { for n in *need { let there = match n.strip_suffix('*') { Some(prefix) => r.as_object().is_some_and(|m| m.keys().any(|k| k.starts_with(prefix) && k.len() > prefix.len() && k[prefix.len()..].starts_with(|c: char| c.is_ascii_uppercase()))), None => r.get(*n).is_some(), }; if !there { p.push(format!("{}.{}: required, and missing", at, n.trim_end_matches('*'))); } } } for (_, field, allowed) in CODES.iter().filter(|c| c.0 == t) { if let Some(v) = r.get(*field) { if !v.as_str().is_some_and(|s| allowed.contains(&s)) { p.push(format!("{}.{}: {} should be one of {}", at, field, v, allowed.join(", "))); } } } match t { "Organization" if r.get("identifier").is_none() && r.get("name").is_none() => p.push(format!("{}: org-1: an Organization needs a name or an identifier", at)), "Observation" if r.get("value").is_some() => p.push(format!("{}.value: write value[x] with its type, such as valueQuantity", at)), "Bundle" => { let kind = r.get("type").and_then(Value::as_str).unwrap_or(""); for (i, e) in r.get("entry").and_then(Value::as_array).into_iter().flatten().enumerate() { let here = format!("{}.entry[{}]", at, i); if let Some(res) = e.get("resource") { check_one(res, &here, p); } if (kind == "transaction" || kind == "batch") && e.get("request").is_none() { p.push(format!("{}: bdl-3: an entry of a {} needs a request", here, kind)); } if kind != "transaction" && kind != "batch" && kind != "transaction-response" && kind != "batch-response" && kind != "history" && e.get("request").is_some() { p.push(format!("{}: bdl-3: only a batch, transaction or history entry has a request", here)); } } if r.get("total").is_some() && kind != "searchset" && kind != "history" { p.push(format!("{}.total: bdl-1: total is only for searchset and history", at)); } let mut ids: Vec<String> = r.get("entry").and_then(Value::as_array).into_iter().flatten().filter_map(|e| e["fullUrl"].as_str().map(str::to_string)).collect(); ids.sort(); ids.dedup_by(|a, b| { if a == b { p.push(format!("{}: bdl-7: fullUrl {} appears twice", at, a)); true } else { false } }); } _ => {} } if t != "Bundle" { walk(&at, r, p); }}/// Checks a resource against FHIR R4's rules for it: required elements, bound codes, id, date and dateTime formats, no empty strings, arrays or nulls, the Bundle rules, and what is `contained`. `{ ok, problems }`, each problem naming its path. Not a full profile validator: the resources most in use are covered, and of those their commonest bindings.pub fn fhir_check(resource: serde_json::Value) -> Value { let mut p = vec![]; check_one(&resource, "", &mut p); json!({ "ok": p.is_empty(), "problems": p })}/// An OperationOutcome, what a FHIR server answers a failure with: `severity` (fatal, error, warning, information), `code` (invalid, not-found, conflict...), and the words.pub fn fhir_outcome(severity: &str, code: &str, diagnostics: &str) -> Value { json!({ "resourceType": "OperationOutcome", "issue": [{ "severity": severity, "code": code, "diagnostics": diagnostics }] })}/// Reads a Bundle: `{ type, total, links, entries: [{ full_url, resource_type, id, resource, search_mode, request, response }] }`.pub fn fhir_bundle_read(bundle: serde_json::Value) -> Result<Value, String> { if bundle["resourceType"] != "Bundle" { return Err(format!("this is a {}, not a Bundle", bundle["resourceType"])); } let links: Map<String, Value> = bundle["link"].as_array().into_iter().flatten().filter_map(|l| Some((l["relation"].as_str()?.to_string(), l["url"].clone()))).collect(); let entries: Vec<Value> = bundle["entry"].as_array().into_iter().flatten().map(|e| json!({ "full_url": e["fullUrl"], "resource_type": e["resource"]["resourceType"], "id": e["resource"]["id"], "resource": e["resource"], "search_mode": e["search"]["mode"], "request": e["request"], "response": e["response"], })).collect(); Ok(json!({ "type": bundle["type"], "total": bundle["total"], "links": links, "entries": entries }))}fn full_url(r: &Value, base: &str, i: usize) -> Value { let (t, id) = (r["resourceType"].as_str().unwrap_or(""), r["id"].as_str().unwrap_or("")); if base.is_empty() || id.is_empty() { // Two lines in place of a uuid crate: an id that only has to be unique in this Bundle. json!(format!("urn:uuid:00000000-0000-4000-8000-{:012x}", i + 1)) } else { json!(format!("{}/{}/{}", base.trim_end_matches('/'), t, id)) }}/// Makes a Bundle of `kind` (collection, transaction, batch, document, searchset...) from resources. For a transaction or batch each entry gets its `request`: PUT by id, POST without one. `base` makes each `fullUrl`; entries without an id get a `urn:uuid:` one, and a reference written `#n`-free as `Patient/temp` keeps working only if you use those fullUrls yourself.pub fn fhir_bundle_build(kind: &str, resources: Vec<serde_json::Value>, base: &str) -> Result<Value, String> { if !CODES.iter().find(|c| c.0 == "Bundle").unwrap().2.contains(&kind) { return Err(format!("`{}` is not a Bundle type", kind)); } let mut entries = vec![]; for (i, r) in resources.iter().enumerate() { let t = r["resourceType"].as_str().ok_or(format!("resource {}: no resourceType", i + 1))?; let mut e = json!({ "fullUrl": full_url(r, base, i), "resource": r }); if kind == "transaction" || kind == "batch" { e["request"] = match r["id"].as_str() { Some(id) => json!({ "method": "PUT", "url": format!("{}/{}", t, id) }), None => json!({ "method": "POST", "url": t }), }; } entries.push(e); } let mut b = json!({ "resourceType": "Bundle", "type": kind }); if kind == "searchset" { b["total"] = json!(entries.len()); } b["entry"] = Value::Array(entries); Ok(b)}/// Finds what a reference points to. `reference` is a text or a Reference map; relative (`Patient/1`), absolute (`https://x/fhir/Patient/1`), `urn:uuid:` or `Patient/1/_history/2`. Looked for in the Bundle by `fullUrl`, then by type and id. `#id` is looked for in `from`'s contained resources. Null when not there.pub fn fhir_resolve(bundle: serde_json::Value, reference: serde_json::Value, from: serde_json::Value) -> Option<Value> { let r = reference.as_str().or(reference["reference"].as_str())?; if let Some(id) = r.strip_prefix('#') { return from["contained"].as_array()?.iter().find(|c| c["id"] == id).cloned(); } let entries = bundle["entry"].as_array()?; if let Some(e) = entries.iter().find(|e| e["fullUrl"] == r) { return Some(e["resource"].clone()); } let clean = r.split("/_history/").next().unwrap_or(r); let mut parts = clean.rsplit('/'); let (id, t) = (parts.next()?, parts.next()?); entries.iter().find(|e| e["resource"]["resourceType"] == t && e["resource"]["id"] == id).map(|e| e["resource"].clone())}/// Every reference in a resource: `[{ path, reference, display }]`.pub fn fhir_references(resource: serde_json::Value) -> Vec<Value> { fn go(path: &str, v: &Value, out: &mut Vec<Value>) { match v { Value::Object(m) => { if let Some(Value::String(r)) = m.get("reference") { out.push(json!({ "path": path, "reference": r, "display": m.get("display") })); } for (k, x) in m { go(&if path.is_empty() { k.clone() } else { format!("{}.{}", path, k) }, x, out); } } Value::Array(a) => { for (i, x) in a.iter().enumerate() { go(&format!("{}[{}]", path, i), x, out); } } _ => {} } } let mut out = vec![]; go("", &resource, &mut out); out}/// Splits on dots outside brackets, parentheses and quotes.fn steps(p: &str) -> Vec<String> { let (mut out, mut cur, mut depth, mut quote) = (vec![], String::new(), 0, false); for c in p.chars() { match c { '\'' => { quote = !quote; cur.push(c) } '(' | '[' if !quote => { depth += 1; cur.push(c) } ')' | ']' if !quote => { depth -= 1; cur.push(c) } '.' if !quote && depth == 0 => out.push(std::mem::take(&mut cur)), o => cur.push(o), } } if !cur.is_empty() { out.push(cur); } out}fn eval_step(items: Vec<Value>, step: &str) -> Result<Vec<Value>, String> { if let Some(cond) = step.strip_prefix("where(").and_then(|s| s.strip_suffix(')')) { let (field, want) = cond.split_once('=').ok_or(format!("where({}) should be where(field='value')", cond))?; let want = want.trim().trim_matches('\''); let mut out = vec![]; for x in items { let got = eval_path(&x, field.trim())?; if got.iter().any(|g| g.as_str() == Some(want) || g.to_string() == want) { out.push(x); } } return Ok(out); } match step { "first()" => return Ok(items.into_iter().take(1).collect()), "last()" => return Ok(items.into_iter().last().into_iter().collect()), "count()" => return Ok(vec![json!(items.len())]), "exists()" => return Ok(vec![json!(!items.is_empty())]), "empty()" => return Ok(vec![json!(items.is_empty())]), _ => {} } if step.ends_with(')') { return Err(format!("`{}` is beyond this small FHIRPath: where(), first(), last(), count(), exists(), empty() and [n] are here", step)); } let (name, index) = match step.split_once('[') { Some((n, i)) => (n, Some(i.trim_end_matches(']').parse::<usize>().map_err(|_| format!("`{}`: an index is a number", step))?)), None => (step, None), }; let mut out = vec![]; for x in &items { let found = x.get(name).cloned().or_else(|| { // value finds valueQuantity: a choice element, by its short name. x.as_object()?.iter().find(|(k, _)| k.starts_with(name) && k[name.len()..].starts_with(|c: char| c.is_ascii_uppercase())).map(|(_, v)| v.clone()) }); match found { Some(Value::Array(a)) => out.extend(a), Some(v) => out.push(v), None => {} } } Ok(match index { Some(i) => out.into_iter().skip(i).take(1).collect(), None => out, })}fn eval_path(r: &Value, path: &str) -> Result<Vec<Value>, String> { let mut items = vec![r.clone()]; for (i, s) in steps(path).iter().enumerate() { if i == 0 && s.as_str() == r["resourceType"].as_str().unwrap_or("\0") { continue; } items = eval_step(items, s)?; } Ok(items)}/// A small FHIRPath: every value at `path`, as a list. `name.family`, `Patient.name[0].given`, `telecom.where(system='phone').value`, `value` for a choice like `valueQuantity`, and `first()`, `last()`, `count()`, `exists()`, `empty()`.pub fn fhir_path(resource: serde_json::Value, path: &str) -> Result<Vec<Value>, String> { eval_path(&resource, path)}/// The first value at `path`, or null.pub fn fhir_get(resource: serde_json::Value, path: &str) -> Result<Value, String> { Ok(eval_path(&resource, path)?.into_iter().next().unwrap_or(Value::Null))}fn dec(x: &str) -> String { let b = x.as_bytes(); let (mut i, mut out) = (0, Vec::with_capacity(b.len())); while i < b.len() { match (b[i], x.get(i + 1..i + 3).and_then(|h| u8::from_str_radix(h, 16).ok())) { (b'%', Some(h)) => { out.push(h); i += 3; } (b'+', _) => { out.push(b' '); i += 1; } (c, _) => { out.push(c); i += 1; } } } String::from_utf8_lossy(&out).into_owned()}fn enc(x: &str) -> String { x.bytes().map(|b| if b.is_ascii_alphanumeric() || b"-_.~:,|".contains(&b) { (b as char).to_string() } else { format!("%{:02X}", b) }).collect()}const PREFIXES: &[&str] = &["eq", "ne", "gt", "lt", "ge", "le", "sa", "eb", "ap"];/// Reads a FHIR search's query text: `{ params: [{ name, modifier, values: [{ prefix, value }] }], count, offset, sort, include, revinclude, summary, elements }`. `birthdate=ge1990-01-01`, `name:exact=Ada`, `code=http://loinc.org|1234-5`, a comma being "or" and a repeated parameter "and".pub fn fhir_search_parse(query: &str) -> Result<Value, String> { let q = query.split_once('?').map_or(query, |x| x.1); let mut params = vec![]; let mut out = json!({ "count": null, "offset": 0, "sort": [], "include": [], "revinclude": [], "summary": null, "elements": [] }); for pair in q.split('&').filter(|p| !p.is_empty()) { let (k, v) = pair.split_once('=').unwrap_or((pair, "")); let (k, v) = (dec(k), dec(v)); let (name, modifier) = k.split_once(':').map_or((k.as_str(), None), |(a, b)| (a, Some(b))); let number = |what: &str| v.parse::<i64>().ok().filter(|n| *n >= 0).ok_or(format!("{} `{}` should be a whole number", what, v)); match name { "_count" => out["count"] = json!(number("_count")?), "_offset" | "_getpagesoffset" => out["offset"] = json!(number("_offset")?), "_sort" => out["sort"] = json!(v.split(',').map(|s| json!({ "field": s.trim_start_matches('-'), "descending": s.starts_with('-') })).collect::<Vec<_>>()), "_include" | "_revinclude" => { let key = name.trim_start_matches('_'); out[key].as_array_mut().unwrap().push(json!(v)); } "_summary" => out["summary"] = json!(v), "_elements" => out["elements"] = json!(v.split(',').collect::<Vec<_>>()), "_format" | "_pretty" => {} _ => { let values: Vec<Value> = v.split(',').map(|x| { let has = x.len() > 2 && PREFIXES.contains(&&x[..2]) && x.as_bytes()[2].is_ascii_digit(); if has { json!({ "prefix": &x[..2], "value": &x[2..] }) } else { json!({ "prefix": "eq", "value": x }) } }).collect(); params.push(json!({ "name": name, "modifier": modifier, "values": values })); } } } out["params"] = Value::Array(params); Ok(out)}/// What a search parameter looks at on a resource: FHIR's definitions of the ones in common use.fn search_paths(t: &str, name: &str) -> Vec<&'static str> { match (t, name) { (_, "_id") => vec!["id"], (_, "_lastUpdated") => vec!["meta.lastUpdated"], ("Patient" | "Practitioner", "family") => vec!["name.family"], ("Patient" | "Practitioner", "given") => vec!["name.given"], ("Patient" | "Practitioner", "name") => vec!["name.family", "name.given", "name.text"], ("Patient" | "Practitioner", "gender") => vec!["gender"], ("Patient", "birthdate") => vec!["birthDate"], ("Patient", "identifier") => vec!["identifier"], ("Patient" | "Practitioner", "telecom") => vec!["telecom.value"], ("Patient" | "Practitioner", "email") => vec!["telecom.where(system='email').value"], ("Patient" | "Practitioner", "phone") => vec!["telecom.where(system='phone').value"], ("Patient", "address-city") => vec!["address.city"], ("Organization", "name") => vec!["name"], ("Observation" | "Condition" | "DiagnosticReport" | "Procedure", "code") => vec!["code.coding"], ("Observation", "date") => vec!["effective"], ("Observation" | "DiagnosticReport" | "Encounter" | "Procedure" | "MedicationRequest" | "Condition", "patient" | "subject") => vec!["subject.reference"], ("Observation" | "DiagnosticReport" | "Encounter" | "Procedure" | "MedicationRequest", "status") => vec!["status"], ("Condition", "clinical-status") => vec!["clinicalStatus.coding.code"], ("Encounter", "date") => vec!["period.start"], ("AllergyIntolerance" | "Immunization", "patient") => vec!["patient.reference"], _ => vec![], }}fn prefix_match(got: &str, prefix: &str, want: &str) -> bool { let (a, b) = (got.parse::<f64>(), want.parse::<f64>()); let ord = match (a, b) { (Ok(x), Ok(y)) => x.partial_cmp(&y).unwrap_or(std::cmp::Ordering::Equal), _ => { // Dates compare to the precision of the shorter, as `1990` covers the year. let n = got.len().min(want.len()); got[..got.floor_char_boundary(n)].cmp(&want[..want.floor_char_boundary(n)]) } }; match prefix { "eq" => ord.is_eq(), "ne" => ord.is_ne(), "gt" | "sa" => ord.is_gt(), "lt" | "eb" => ord.is_lt(), "ge" => ord.is_ge(), "le" => ord.is_le(), _ => ord.is_eq(), }}/// One value against a search value, by the kind of thing it is: strings start with it, ignoring case; token `system|code` against a Coding or Identifier; a reference by its end.fn value_match(got: &Value, name: &str, modifier: Option<&str>, prefix: &str, want: &str) -> bool { if let Some(m) = got.as_object() { if m.contains_key("system") || m.contains_key("code") || m.contains_key("value") { let (sys, code) = want.split_once('|').unwrap_or(("", want)); let c = m.get("code").or(m.get("value")).and_then(Value::as_str).unwrap_or(""); let s = m.get("system").and_then(Value::as_str).unwrap_or(""); return c == code && (!want.contains('|') || sys.is_empty() || s == sys); } return false; } let Some(text) = got.as_str().map(str::to_string).or_else(|| got.as_f64().map(|n| n.to_string())) else { return false }; if name == "patient" || name == "subject" { return text == want || text.ends_with(&format!("/{}", want)); } match modifier { Some("exact") => text == want, Some("contains") => text.to_lowercase().contains(&want.to_lowercase()), _ if want.as_bytes().first().is_some_and(u8::is_ascii_digit) || prefix != "eq" => prefix_match(&text, prefix, want), _ => text.to_lowercase().starts_with(&want.to_lowercase()), }}/// Runs a search over `resources` as a FHIR server would, and gives a searchset Bundle: `query` as in `fhir_search_parse`, `resource_type` the kind searched, `base` the server's address. `_count` (default 20, at most 500) and `_offset` page it, with `self`, `first`, `previous`, `next` and `last` links. `_sort` by a field, `-` for descending; `_include` brings referenced resources, as entries with mode `include`. An unknown parameter is an error, as `Prefer: handling=strict` asks.pub fn fhir_search(resource_type: &str, resources: Vec<serde_json::Value>, query: &str, base: &str) -> Result<Value, String> { let q = fhir_search_parse(query)?; let mut hits: Vec<Value> = resources.iter().filter(|r| r["resourceType"] == resource_type).cloned().collect(); for p in q["params"].as_array().into_iter().flatten() { let name = p["name"].as_str().unwrap_or(""); let paths = search_paths(resource_type, name); if paths.is_empty() { return Err(format!("{} has no search parameter `{}` here", resource_type, name)); } let modifier = p["modifier"].as_str(); if let Some(m) = modifier { if m == "missing" { let want = p["values"][0]["value"] == "true"; hits.retain(|r| paths.iter().all(|x| eval_path(r, x).map_or(true, |v| v.is_empty())) == want); continue; } if !["exact", "contains"].contains(&m) { return Err(format!("modifier `:{}` is not supported here", m)); } } let mut kept = vec![]; for r in hits { let mut any = false; for path in &paths { for got in eval_path(&r, path)? { any = any || p["values"].as_array().unwrap().iter().any(|v| value_match(&got, name, modifier, v["prefix"].as_str().unwrap_or("eq"), v["value"].as_str().unwrap_or(""))); } } if any { kept.push(r); } } hits = kept; } for s in q["sort"].as_array().into_iter().flatten().rev() { let field = s["field"].as_str().unwrap_or(""); let paths = search_paths(resource_type, field); let path = paths.first().copied().unwrap_or(field); let desc = s["descending"].as_bool().unwrap_or(false); let key = |r: &Value| eval_path(r, path).ok().and_then(|v| v.into_iter().next()).map(|v| v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string())).unwrap_or_default(); hits.sort_by(|a, b| { let o = key(a).cmp(&key(b)); if desc { o.reverse() } else { o } }); } let total = hits.len() as i64; let count = q["count"].as_i64().unwrap_or(20).clamp(0, 500); let offset = q["offset"].as_i64().unwrap_or(0); let page: Vec<Value> = hits.iter().skip(offset as usize).take(count as usize).cloned().collect(); let base = base.trim_end_matches('/'); let rest: Vec<String> = query.split_once('?').map_or(query, |x| x.1).split('&').filter(|p| !p.is_empty() && !p.starts_with("_offset=") && !p.starts_with("_count=")).map(str::to_string).collect(); let link = |o: i64| { let mut q = rest.clone(); q.push(format!("_count={}", count)); q.push(format!("_offset={}", o)); format!("{}/{}?{}", base, resource_type, q.join("&")) }; let mut links = vec![json!({ "relation": "self", "url": link(offset) }), json!({ "relation": "first", "url": link(0) })]; if offset > 0 && count > 0 { links.push(json!({ "relation": "previous", "url": link((offset - count).max(0)) })); } if count > 0 && offset + count < total { links.push(json!({ "relation": "next", "url": link(offset + count) })); } if count > 0 && total > 0 { links.push(json!({ "relation": "last", "url": link((total - 1) / count * count) })); } let entry_of = |r: &Value, mode: &str| json!({ "fullUrl": format!("{}/{}/{}", base, r["resourceType"].as_str().unwrap_or(""), r["id"].as_str().unwrap_or("")), "resource": r, "search": { "mode": mode } }); let mut entries: Vec<Value> = page.iter().map(|r| entry_of(r, "match")).collect(); let mut seen: Vec<String> = vec![]; for inc in q["include"].as_array().into_iter().flatten() { let name = inc.as_str().unwrap_or("").split(':').nth(1).unwrap_or(""); for r in &page { for path in search_paths(resource_type, name) { for got in eval_path(r, path)? { let Some(refr) = got.as_str() else { continue }; let mut bits = refr.rsplit('/'); let (id, t) = (bits.next().unwrap_or(""), bits.next().unwrap_or("")); let key = format!("{}/{}", t, id); if seen.contains(&key) { continue; } if let Some(x) = resources.iter().find(|x| x["resourceType"] == t && x["id"] == id) { seen.push(key); entries.push(entry_of(x, "include")); } } } } } Ok(json!({ "resourceType": "Bundle", "type": "searchset", "total": total, "link": links, "entry": entries }))}/// The query text of a request's parsed query, for `fhir_search`: `fhir_query_text(request.query)`. A map holds each parameter once.pub fn fhir_query_text(query: std::collections::BTreeMap<String, String>) -> String { query.iter().map(|(k, v)| format!("{}={}", enc(k), enc(v))).collect::<Vec<_>>().join("&")}/// `base?params` for a client: a map of parameters to a search address, values percent-encoded.pub fn fhir_search_url(base: &str, resource_type: &str, params: std::collections::BTreeMap<String, String>) -> String { let q: Vec<String> = params.iter().map(|(k, v)| format!("{}={}", k, enc(v))).collect(); format!("{}/{}?{}", base.trim_end_matches('/'), resource_type, q.join("&"))}Related
json_patch.qwer · 1.0.0JSON Patch (RFC 6902) and Merge Patch (RFC 7396): the difference between two values as operations, and patches applied.10 views · 6 downloadsJSON Schema json_schema.qwer · 1.0.0Check values against a JSON Schema (drafts 4 to 2020-12), with every error and the path of the value at fault.8 views · 8 downloadsJSONPath jsonpath.qwer · 1.0.0Query values with JSONPath (RFC 9535): filters, slices, recursive descent and functions, with the paths of what matched.8 views · 7 downloadsCanonical JSON json_canonical.qwer · 1.0.0Canonical JSON (RFC 8785): one way to write a value, keys sorted and numbers as JavaScript writes them, so hashes and signatures of JSON hold however it was spaced or ordered.7 views · 5 downloadsDICOM dicom.qwer · 1.0.0DICOM medical image files read by hand, explicit and implicit VR: tags by name, patient, study and series fields, identifying data stripped by a good part of the basic profile and the file written back, and uncompressed pixels to PNG with rescale and window applied.5 views · 9 downloadsHL7 hl7.qwer · 1.0.0HL7 v2 messages from hospitals and labs: segments, fields, components and repetitions read, values picked by path, and ACKs made.7 views · 8 downloadsThread 0 posts
On FHIR: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.