Skip to content

Commit

Permalink
feat: Implement base traits for all resources
Browse files Browse the repository at this point in the history
  • Loading branch information
FlixCoder committed Mar 25, 2023
1 parent 8267002 commit 580af42
Show file tree
Hide file tree
Showing 7 changed files with 17,355 additions and 32 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ This is a [FHIR](https://www.hl7.org/fhir/) library in its early stages. The mod
- [x] Builders for types and resources
- [x] Allow to convert code enums to Coding and CodeableConcept
- [ ] Implementation of base traits
- [ ] Resource, including FHIR version, resource type (const)
- [ ] DomainResource
- [x] (Base)Resource
- [x] NamedResource
- [x] DomainResource
- [ ] IdentifiableResource
- [ ] FHIR client implmentation
- [ ] FHIRpath implementation
Expand Down
16,990 changes: 16,990 additions & 0 deletions crates/fhir-model/src/r4b/resources/generated.rs

Large diffs are not rendered by default.

17 changes: 15 additions & 2 deletions crates/fhir-model/tests/test_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use assert_json_diff::{assert_json_matches, CompareMode, Config, NumericMode};
use fhir_model::r4b::{
codes::{RequestIntent, RequestStatus, RiskProbability},
resources::{
Basic, Patient, RequestGroup, RequestGroupAction, RequestGroupActionTiming, Resource,
WrongResourceType,
Basic, NamedResource, Patient, RequestGroup, RequestGroupAction, RequestGroupActionTiming,
Resource, WrongResourceType,
},
types::{CodeableConcept, Coding},
};
Expand Down Expand Up @@ -78,3 +78,16 @@ fn coding_concepts() {
assert_eq!(concept.coding.len(), 1);
assert!(concept.text.is_some());
}

#[test]
fn resource_traits() {
let ty = Patient::TYPE;
let mut patient: Resource = Patient::builder().id("1".to_owned()).build().into();
assert_eq!(patient.resource_type(), ty);

assert!(patient.as_base_resource().id().is_some());
assert!(patient.as_domain_resource().is_some());

patient.as_base_resource_mut().set_id(None);
assert!(patient.as_base_resource().id().is_none());
}
267 changes: 267 additions & 0 deletions crates/generate/src/generate/gen_traits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
//! Generate traits for base resource types.

use anyhow::{anyhow, Result};
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};

use super::{
gen_types::{code_field_type_name, construct_field_type},
map_field_ident, map_type,
};
use crate::structures::{Field, Type, TypeKind};

/// Generate the BaseResource trait and its implementations.
pub fn generate_base_resource(
resources: &[Type],
implemented_codes: &[String],
) -> Result<TokenStream> {
let resource = resources
.iter()
.filter(|ty| ty.r#abstract)
.filter(|ty| ty.kind == TypeKind::Resource)
.find(|ty| &ty.name == "Resource")
.ok_or(anyhow!("Could not find base Resource definition"))?;
let (field_names, field_types) =
get_field_names_and_types(&resource.elements.fields, implemented_codes);

let ident = format_ident!("BaseResource");
let trait_definition = make_trait_definition(resource, &field_names, &field_types, &ident);

let trait_implementations: TokenStream = resources
.iter()
.filter(|ty| !ty.r#abstract)
.filter(|ty| ty.kind == TypeKind::Resource)
.map(|ty| make_trait_implementation(ty, &field_names, &field_types, &ident))
.collect();

let filtered_resources: Vec<_> = resources
.iter()
.filter(|ty| !ty.r#abstract)
.filter(|ty| ty.kind == TypeKind::Resource)
.map(|ty| format_ident!("{}", ty.name))
.collect();
let impl_resource_as_trait = quote! {
impl Resource {
/// Return the resource as base resource.
#[must_use]
pub fn as_base_resource(&self) -> &dyn #ident {
match self {
#(
Self::#filtered_resources(r) => r,
)*
}
}

/// Return the resource as mutable base resource.
#[must_use]
pub fn as_base_resource_mut(&mut self) -> &mut dyn #ident {
match self {
#(
Self::#filtered_resources(r) => r,
)*
}
}
}
};

Ok(quote! {
#trait_definition
#trait_implementations
#impl_resource_as_trait
})
}

/// Generate the DomainResource trait and its implementations.
pub fn generate_domain_resource(
resources: &[Type],
implemented_codes: &[String],
) -> Result<TokenStream> {
let resource = resources
.iter()
.filter(|ty| ty.r#abstract)
.filter(|ty| ty.kind == TypeKind::Resource)
.find(|ty| &ty.name == "DomainResource")
.ok_or(anyhow!("Could not find DomainResource definition"))?;
let (field_names, field_types) =
get_field_names_and_types(&resource.elements.fields, implemented_codes);

let ident = format_ident!("DomainResource");
let trait_definition = make_trait_definition(resource, &field_names, &field_types, &ident);

let trait_implementations: TokenStream = resources
.iter()
.filter(|ty| !ty.r#abstract)
.filter(|ty| ty.kind == TypeKind::Resource)
.filter(|ty| ty.base.as_ref().map_or(false, |base| base.ends_with("DomainResource")))
.map(|ty| make_trait_implementation(ty, &field_names, &field_types, &ident))
.collect();

let filtered_resources: Vec<_> = resources
.iter()
.filter(|ty| !ty.r#abstract)
.filter(|ty| ty.kind == TypeKind::Resource)
.filter(|ty| ty.base.as_ref().map_or(false, |base| base.ends_with("DomainResource")))
.map(|ty| format_ident!("{}", ty.name))
.collect();
let impl_resource_as_trait = quote! {
impl Resource {
/// Return the resource as domain resource.
#[must_use]
pub fn as_domain_resource(&self) -> Option<&dyn #ident> {
match self {
#(
Self::#filtered_resources(r) => Some(r),
)*
_ => None,
}
}

/// Return the resource as mutable domain resource.
#[must_use]
pub fn as_domain_resource_mut(&mut self) -> Option<&mut dyn #ident> {
match self {
#(
Self::#filtered_resources(r) => Some(r),
)*
_ => None,
}
}
}
};

Ok(quote! {
#trait_definition
#trait_implementations
#impl_resource_as_trait
})
}

/// Generate the NamedResource trait and its implementations.
pub fn generate_named_resource(resources: &[Type]) -> Result<TokenStream> {
let trait_definition = quote! {
/// Simple trait to supply (const) information about resources.
pub trait NamedResource {
/// The FHIR version of this resource.
const FHIR_VERSION: &'static str;
/// The ResourceType of this resouce.
const TYPE: ResourceType;
}
};

let trait_implementations: TokenStream = resources
.iter()
.filter(|ty| !ty.r#abstract)
.filter(|ty| ty.kind == TypeKind::Resource)
.map(|ty| {
let name = format_ident!("{}", ty.name);
let version = &ty.version;

quote! {
impl NamedResource for #name {
const FHIR_VERSION: &'static str = #version;
const TYPE: ResourceType = ResourceType::#name;
}
}
})
.collect();

Ok(quote! {
#trait_definition
#trait_implementations
})
}

/// Get field names and types from the elements.
fn get_field_names_and_types(
fields: &[Field],
implemented_codes: &[String],
) -> (Vec<Ident>, Vec<TokenStream>) {
fields
.iter()
.cloned()
.map(|mut field| {
field.set_base_field();
let field_name = map_field_ident(field.name());
let field_type = match &field {
Field::Standard(f) => {
let ty = map_type(&f.r#type);
quote!(#ty)
}
Field::Code(f) => code_field_type_name(f, implemented_codes),
_ => panic!("Unsupported field type in BaseResource!"),
};
(field_name, construct_field_type(&field, field_type))
})
.unzip()
}

/// Make a trait definition from the resource, field names and types for the
/// given trait name.
fn make_trait_definition(
resource: &Type,
field_names: &[Ident],
field_types: &[TokenStream],
ident: &Ident,
) -> TokenStream {
let mut doc_comment = format!(
" # {} \n\n {} \n\n ## {} (FHIR version: {}) \n\n {} \n\n {} \n\n ",
resource.name,
resource.description.replace('\r', "\n"),
resource.elements.name,
resource.version,
resource.elements.short.replace('\r', "\n"),
resource.elements.definition.replace('\r', "\n")
);
if let Some(comment) = &resource.elements.comment {
doc_comment.push_str(&comment.replace('\r', "\n"));
doc_comment.push(' ');
}

let mut_getters = field_names.iter().map(|name| format_ident!("{name}_mut"));
let setters = field_names.iter().map(|name| format_ident!("set_{name}"));

quote! {
#[doc = #doc_comment]
pub trait #ident {
#(
#[doc = concat!("Get `", stringify!(#field_names), "`.")]
fn #field_names(&self) -> & #field_types;
#[doc = concat!("Get `", stringify!(#field_names), "` mutably.")]
fn #mut_getters(&mut self) -> &mut #field_types;
#[doc = concat!("Set `", stringify!(#field_names), "`.")]
fn #setters(&mut self, value: #field_types);
)*
}
}
}

/// Make a trait implementation for the resource, given the field names and
/// types for the given trait name.
fn make_trait_implementation(
resource: &Type,
field_names: &[Ident],
field_types: &[TokenStream],
ident: &Ident,
) -> TokenStream {
let name = format_ident!("{}", resource.name);
let mut_getters = field_names.iter().map(|name| format_ident!("{name}_mut"));
let setters = field_names.iter().map(|name| format_ident!("set_{name}"));

quote! {
impl #ident for #name {
#(
fn #field_names(&self) -> & #field_types {
&self.#field_names
}

fn #mut_getters(&mut self) -> &mut #field_types {
&mut self.#field_names
}

fn #setters(&mut self, value: #field_types) {
self.#field_names = value;
}
)*
}
}
}
47 changes: 28 additions & 19 deletions crates/generate/src/generate/gen_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,17 +134,7 @@ fn generate_field(

let name = field.name().replace("[x]", "");
let ident = map_field_ident(&name);
let ty = if field.is_array() {
if field.is_base_field() {
quote!(Vec<#field_type>)
} else {
quote!(Vec<Option<#field_type>>)
}
} else if field.optional() {
quote!(Option<#field_type>)
} else {
quote!(#field_type)
};
let ty = construct_field_type(field, field_type);

let serde_attr = field.optional().then(|| {
if field.is_array() {
Expand Down Expand Up @@ -226,14 +216,7 @@ fn generate_code_field(
doc_comment.push(' ');
}

let mapped_type = if field.r#type.as_str() == "code" && implemented_codes.contains(&field.code)
{
let ty = format_ident!("{}", field.code);
quote!(codes::#ty)
} else {
let mapped_type = map_type(&field.r#type);
quote!(#mapped_type)
};
let mapped_type = code_field_type_name(field, implemented_codes);

(doc_comment, mapped_type, quote!())
}
Expand Down Expand Up @@ -332,3 +315,29 @@ fn generate_object_field(
.expect("Cannot fail");
(doc_comment, quote!(#struct_type), structs)
}

/// Construct the type of a field.
pub fn construct_field_type(field: &Field, field_type: TokenStream) -> TokenStream {
if field.is_array() {
if field.is_base_field() {
quote!(Vec<#field_type>)
} else {
quote!(Vec<Option<#field_type>>)
}
} else if field.optional() {
quote!(Option<#field_type>)
} else {
quote!(#field_type)
}
}

/// Compute the type name of a CodeField.
pub fn code_field_type_name(field: &CodeField, implemented_codes: &[String]) -> TokenStream {
if field.r#type.as_str() == "code" && implemented_codes.contains(&field.code) {
let ty = format_ident!("{}", field.code);
quote!(codes::#ty)
} else {
let mapped_type = map_type(&field.r#type);
quote!(#mapped_type)
}
}
Loading

0 comments on commit 580af42

Please sign in to comment.