Giter VIP home page Giter VIP logo

fhir-sdk's Introduction

FHIR SDK

crates.io page docs.rs page license: MIT

This is a FHIR library in its early stages. The models are generated from the FHIR StructureDefinitions (see FHIR downloads). It aims to be:

  • fully compliant
  • as safe as possibe
  • as easy to use as possible
  • fully featured

Features

  • Generated FHIR codes, types and resources
  • Serialization and deserialization to and from JSON
  • Optional builders for types and resources
  • Implementation of base traits
    • (Base)Resource for accessing common fields
    • NamedResource for getting the resource type in const time
    • DomainResource for accessing common fields
    • IdentifiableResource for all resources with an identifier field
  • Client implementation
    • Create, Read, Update, Delete
    • Search + Paging
    • Batch operations / Transactions
    • Authentication callback
    • Operations
    • Patch
    • GraphQL
  • FHIRpath implementation
  • Resource validation using FHIRpath and regular expressions

Not Planned

Example

use fhir_sdk::r5::resources::Patient;
use fhir_sdk::client::{*, r5::*};
use fhir_sdk::TryStreamExt;

#[tokio::main]
async fn main() -> Result<(), Error> {
    // Set up the client using the local test server.
    let settings = RequestSettings::default()
        .header(header::AUTHORIZATION, "Bearer <token>".parse().unwrap());
    let client = Client::builder()
        .base_url("http://localhost:8100/fhir/".parse().unwrap())
        .request_settings(settings)
        .build()?;

    // Create a Patient resource using a builder.
    let mut patient = Patient::builder().active(false).build().unwrap();
    // Push it to the server.
    patient.create(&client).await?;
    // The id and versionId is updated automatically this way.
    assert!(patient.id.is_some());
    
    // Search for all patient with `active` = false, including pagination.
    let patients: Vec<Patient> = client
        .search(SearchParameters::empty().and(TokenSearch::Standard {
            name: "active",
            system: None,
            code: Some("false"),
            not: false,
        }))
        .try_collect()
        .await?;

    Ok(())
}

For more examples, see the tests or below.

Testing

Simply set up the FHIR test environment using cargo xtask docker -- up -d and then run cargo xtask test. If you need sudo to run docker, use the --sudo or just -s flag on cargo xtask docker.

Known Problems

  • The compile time and its memory usage are really high. This is due to the big serde derives being highly generic. It might be possible to shave some off by manually implementing Deserialize and Serialize, but that is complex.
  • Vec<Option<T>> is annoying, but sadly is required to allow [null, {...}, null] for using FHIR resources with extensions..
  • It is not supported to replace required fields by an extension.

More examples

Reading a resource from string/file

use fhir_sdk::r5::resources::Resource;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let resource_str = r#"{
        "resourceType": "Patient",
        "id": "my-id",
        "birthDate": "2024-01-01"
    }"#;
    let _resource: Resource = serde_json::from_str(resource_str)?;
    Ok(())
}

Authentication callback

use fhir_sdk::r5::resources::Patient;
use fhir_sdk::client::*;

async fn my_auth_callback() -> Result<HeaderValue, eyre::Report> {
    Ok(HeaderValue::from_static("Bearer <token>"))
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    // Set up the client using the local test server.
    let client = Client::builder()
        .base_url("http://localhost:8100/fhir/".parse().unwrap())
        .auth_callback(my_auth_callback)
        .build()?;

    // Create a Patient resource using a builder.
    let mut patient = Patient::builder().active(false).build().unwrap();
    // Push it to the server. On unauthorized failures, the client will call our
    // auth_callback method to refresh the authorization.
    patient.create(&client).await?;

    Ok(())
}

Resource identifier access

use fhir_sdk::r5::{
    codes::AdministrativeGender,
    resources::{IdentifiableResource, Patient},
    types::{Identifier, HumanName},
};

#[tokio::main]
async fn main() {
    // Create a Patient resource using a builder.
    let mut patient = Patient::builder()
        .active(false)
        .identifier(vec![Some(
            Identifier::builder()
                .system("MySystem".to_owned())
                .value("ID".to_owned())
                .build()
                .unwrap()
        )])
        .gender(AdministrativeGender::Male)
        .name(vec![Some(HumanName::builder().family("Test".to_owned()).build().unwrap())])
        .build()
        .unwrap();

    // Check the identifier value.
    assert_eq!(patient.identifier_with_system("MySystem").map(String::as_str), Some("ID"));
}

License

Licensed under the MIT license. All contributors agree to license under this license.

fhir-sdk's People

Contributors

flixcoder avatar jarimayenburg avatar revanee avatar skomski avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

fhir-sdk's Issues

`ClientBuilder` requires the `auth_callback` to be `Copy`

The ClientBuilder requires the auth_callback being passed to be Copy. This makes it difficult to manage the credentials being used, as Copy closures need to only contain Copy references. If we have a client_id and client_secret as Strings, they cannot be owned by the function being passed. We also cannot use Arc references to them, since Arc also isn't Copy. The possible workarounds seem to be either using 'static references, or having to keep track of lifetimes, neither of which is ideal.

I would suggest changing the Copy requirement to Clone, as this would still allow usage of Copy closures, but also allow having credentials owned by the auth_callback.

Duplicate `resourceType` entry when serializing `Resource` enum to json

fhir_model::r4b::reources::generated::Resource enum has a duplicate resourceType field when serialized to json.

Given the following exampe:

#[test]
fn test_serialization() {
	let subscription_status = SubscriptionStatus::builder()
		.r#type(SubscriptionNotificationType::EventNotification)
		.subscription(Reference::builder().build().unwrap())
		.build()
		.unwrap();
	println!("SubscriptionStatus: {}", serde_json::ser::to_string(&subscription_status).unwrap());
	let resource = Resource::SubscriptionStatus(subscription_status);
	println!("Resource: {}", serde_json::ser::to_string(&resource).unwrap());
}

The SubscriptionStatus is serialized to

{"resourceType":"SubscriptionStatus","type":"event-notification","subscription":{}}

When wrapped in Resource::SubscriptionStatus, it is serialized to

{"resourceType":"SubscriptionStatus","resourceType":"SubscriptionStatus","type":"event-notification","subscription":{}}

Notice the duplicate resourceType in the json.

It seems to be caused by this serde tag at fhir-model/src/r4b/resources/generated.rs:114923

#[serde(tag = "resourceType")]

When changed to this, it serializes correctly.

#[serde(untagged)]

And the result:

{"resourceType":"SubscriptionStatus","type":"event-notification","subscription":{}}

[SUGGESTION] Split generated files

Hi, I'm wondering if it would be better to split the generated files in multiple files so that the compiler can handle them without taking 8+ GBs of RAM to compile.

Support extension-only fields on required fields?

FHIR spec seems to allow setting an extension instead of setting the required field value. This library currently requires to set the field value, regardless of whether the extension is set. It is difficult to represent it in a nice way, so for now it is just ignored. At some point full FHIR compliance would be nice and important though.

Custom Profile Support

Support for custom profiles would be a nice to have.

https://www.hl7.org/fhir/profiling.html

Although, this would require thinking about the implementation, because you would need to generate the structs in the consuming service, so this could be quite a fundimental change.

Something like how prost-builder generates structs from protcol buffer files in the build.rs file of the consuming project could be nice. It could also help with build times as the generated types would sit in the consumers project and would be seporated from the sdk-client code.

This might not be the best approach though.

Derive Hash for ResourceType

Deriving Hash for ResourceType (and maybe for other structs, too) would allow to use ResourceType as key in a HashMap.

Reduce code duplication across FHIR versions

#18 introduced a lot of client implementation detail duplication due to different resource types and such. Maybe it can be reduced a bit as it is mostly copied code. A macro is usually bad to maintain though, so that might or might not be a solution..

[SUGGESTION] Wrappers/helpers for data creation/editing of FHIR resources

Right now it can be pretty difficult to create FHIR resources directly with Rust code.

Rust example:

let mut patient = Patient::builder().build();

let family_name = HumanName::builder().family(String::from("Family")).build();
let given_name = HumanName::builder().given(Vec::from([Some(String::from("Given"))])).build();

patient.name = vec![Some(family_name), Some(given_name)];

While for C# FHIR SDK it's like:

var patient = new Patient();
patient.Name = new List<HumanName>
{
    new HumanName
    {
        Family = "Family",
        Given = new List<string>
        {
            "Given"
        }
    }
};

My suggestion would be to either have a helper/wrapper for all fields, or if possible to do it like C# where you basically write it as JSON (I don't know if it's possible).

Suggestion 1

Have helpers for all fields

let mut patient = Patient::builder().build();

patient.set_family_name(String::from("Family");
patient.set_given_name(Vec::from([Some(String::from("Given"))]));

Suggestion 2

If possible have it similar to C#.

NOTE! Not valid code, just a suggestion as I really like this kind of syntax.

let mut patient = Patient::builder().build();

patient.name = {
  family = String::from("Family"),
  given = vec![Some(String::from("Given))]
};

Add CI

Some checks in GitHub workflows are nice, but the standard runners do not have enough RAM. Would need paid plans or something..

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.