def _validate_field(msg: message.Message, field: descriptor.FieldDescriptor, field_name: str, primitive_handler_: primitive_handler.PrimitiveHandler): """Validates that required fields are set, and performs basic temporal checks. Args: msg: The Message that the field belongs to. field: The FieldDescriptor of the field to examine. field_name: The name of the field. primitive_handler_: Responsible for returning PrimitiveWrappers. Raises: fhir_errors.InvalidFhirError: In the event that a required field is not set or if temporal requirements are not met. """ if annotation_utils.field_is_required( field) and not proto_utils.field_is_set(msg, field): raise fhir_errors.InvalidFhirError( f'Required field `{field.full_name}` is missing.') if annotation_utils.is_reference(field.message_type): _validate_reference_field(msg, field) return if field.type == descriptor.FieldDescriptor.TYPE_MESSAGE: # Returns the first value only for a singular field for i in range(proto_utils.field_content_length(msg, field)): submessage = proto_utils.get_value_at_field_index(msg, field, i) _validate_fhir_constraints(submessage, field_name, primitive_handler_) # Run extra validation for some types, until FHIRPath validation covers # these as well if fhir_types.is_period(submessage): _validate_period(submessage, field_name)
def testAppendValueAtField_repeatedCompositeValue_appendsValue(self): """Test append_value_at_field with a repeated composite type.""" patient = patient_pb2.Patient() patient_names = [ datatypes_pb2.HumanName(text=datatypes_pb2.String(value="Foo")), datatypes_pb2.HumanName(text=datatypes_pb2.String(value="Bar")), datatypes_pb2.HumanName(text=datatypes_pb2.String(value="Bats")), ] self.assertEqual(proto_utils.field_content_length(patient, "name"), 0) for name in patient_names: proto_utils.append_value_at_field(patient, "name", name) self.assertEqual(proto_utils.field_content_length(patient, "name"), 3) self.assertEqual(patient.name[:], patient_names)
def _validate_reference_field(parent: message.Message, field: descriptor.FieldDescriptor): """Ensure that the provided reference field is valid. Args: parent: The containing Message. field: The reference field descriptor. Raises: fhir_errors.InvalidFhirError: In the event of an empty reference (no extensions, no identifier, no display). """ oneof = field.message_type.oneofs[0] # Singular fields have a length of 1 for i in range(proto_utils.field_content_length(parent, field)): reference = proto_utils.get_value_at_field_index(parent, field, i) reference_field_name = reference.WhichOneof(oneof.name) if reference_field_name is None: if not (reference.extension or reference.HasField('identifier') or reference.HasField('display')): raise fhir_errors.InvalidFhirError( f'`{reference.DESCRIPTOR.name}` is an empty reference.') # There's no reference field, but there is other data. This is valid. return field_options = field.GetOptions() if not field_options.Extensions[annotations_pb2.valid_reference_type]: # The reference field does not have restrictions, so any value is fine. return if reference.HasField('uri') or reference.HasField('fragment'): # Uri and Fragment references are untyped. return # There are no reference annotations for DSTU2; skip validation if annotation_utils.get_fhir_version( reference) == annotations_pb2.FhirVersion.DSTU2: return reference_field = reference.DESCRIPTOR.fields_by_name[ reference_field_name] if annotation_utils.is_typed_reference_field(reference_field): # Ensure that the reference type is listed as "valid" reference_type = reference_field.GetOptions().Extensions[ annotations_pb2.referenced_fhir_type] is_allowed = False for valid_type in field_options.Extensions[ annotations_pb2.valid_reference_type]: if valid_type == reference_type or valid_type == 'Resource': is_allowed = True break if not is_allowed: raise fhir_errors.InvalidFhirError( f'Message `{parent.DESCRIPTOR.full_name}` contains an invalid ' f'reference type: `{reference_type}` set at: ' f'`{reference_field_name}`.')
def _add_fields_to_extension(msg: message.Message, extension: message.Message): """Adds the fields from message to extension.""" for field in msg.DESCRIPTOR.fields: _verify_field_is_proto_message_type(field) # Add submessages to nested extensions; singular fields have a length of 1 for i in range(proto_utils.field_content_length(msg, field)): child_extension = proto_utils.set_in_parent_or_add( extension, 'extension') cast(Any, child_extension).url.value = get_inlined_extension_url(field) value = proto_utils.get_value_at_field_index(msg, field, i) _add_value_to_extension( value, child_extension, annotation_utils.is_choice_type_field(field))
def testFieldContentLength_withNonExistentField_returnsZero(self): """Test field_content_length functionality on non-existent field input.""" default_patient = patient_pb2.Patient() # Leave active unset self.assertEqual( proto_utils.field_content_length(default_patient, "active"), 0)
def testFieldContentLength_withSingularField_returnsSingleCount(self): """Test field_content_length functionality on singular field input.""" patient = patient_pb2.Patient(active=datatypes_pb2.Boolean(value=True)) self.assertEqual(proto_utils.field_content_length(patient, "active"), 1)
def testFieldContentLength_withRepeatedField_returnsContentLength(self): """Test field_content_length functionality on repeated field input.""" patient = self._create_patient_with_names(["A", "B", "C"]) self.assertEqual(proto_utils.field_content_length(patient, "name"), 3)
def _merge_field(self, json_value: Any, field: descriptor.FieldDescriptor, parent: message.Message): """Merges the json_value into the provided field of the parent Message. Args: json_value: The JSON value to set. field: The FieldDescriptor of the field to set in parent. parent: The parent Message to set the value on. Raises: ValueError: In the event that a non-primitive field has already been set. ValueError: In the event that a oneof field has already been set. """ # If the field is non-primitive, ensure that it hasn't been set yet. Note # that we allow primitive types to be set already, because FHIR represents # extensions to primitives as separate, subsequent JSON elements, with the # field prepended by an underscore if (not annotation_utils.is_primitive_type(field.message_type) and proto_utils.field_is_set(parent, field)): raise ValueError('Target field {} is already set.'.format( field.full_name)) # Consider it an error if a oneof field is already set. # # Exception: When a primitive in a choice type has a value and an extension, # it will get set twice, once by the value (e.g. valueString) and once by an # extension (e.g., _valueString) if field.containing_oneof is not None: oneof_field = parent.DESCRIPTOR.oneofs_by_name[ field.containing_oneof.name] if (annotation_utils.is_primitive_type(field.message_type) and oneof_field.full_name == field.full_name): raise ValueError( 'Cannot set field {} since oneof field {} is already set.'. format(field.full_name, oneof_field.full_name)) # Ensure that repeated fields get proper list assignment existing_field_size = proto_utils.field_content_length(parent, field) if proto_utils.field_is_repeated(field): if not isinstance(json_value, list): raise ValueError( f'Attempted to merge a repeated field, {field.name}, a json_value ' f'with type {type(json_value)} instead of a list.') if existing_field_size != 0 and existing_field_size != len( json_value): raise ValueError( 'Repeated primitive list length does not match extension list for ' 'field: {field.full_name!r}.') # Set the JSON values, taking care to clear the PRIMITIVE_HAS_NO_VALUE_URL # if we've already visited the field before. json_value = (json_value if proto_utils.field_is_repeated(field) else [json_value]) for (i, value) in enumerate(json_value): parsed_value = self._parse_field_value(field, value) if existing_field_size > 0: field_value = proto_utils.get_value_at_field_index( parent, field, i) field_value.MergeFrom(parsed_value) extensions.clear_fhir_extensions_with_url( field_value, extensions.PRIMITIVE_HAS_NO_VALUE_URL) else: field_value = proto_utils.set_in_parent_or_add(parent, field) field_value.MergeFrom(parsed_value)
def add_extension_to_message(extension: message.Message, msg: message.Message): """Recursively parses extension and adds to message. Args: extension: The FHIR extension to serialize and add. msg: The message to add the extension onto Raises: InvalidFhirError: In the event that a value is set on the extension, but the corresponding message field to copy it to is repeated (extension values are singular only). """ desc = msg.DESCRIPTOR fields_by_url = { get_inlined_extension_url(field): field for field in desc.fields if field.name != 'id' } # Copy the id field if present id_field = desc.fields_by_name.get('id') if proto_utils.field_is_set(extension, id_field): proto_utils.set_value_at_field(msg, id_field, cast(Any, extension).id) # Handle simple extensions (only one value present) if proto_utils.field_is_set(extension, 'value'): if len(fields_by_url) != 1: raise fhir_errors.InvalidFhirError( f'Expected a single field, found {len(fields_by_url)}; ' f'{desc.full_name} is an invalid extension type.') field = list(fields_by_url.items())[0][1] if proto_utils.field_is_repeated(field): raise fhir_errors.InvalidFhirError( f'Expected {field.full_name} to be a singular field. ' f'{desc.full_name} is an invalid extension type.') _add_extension_value_to_message(extension, msg, field) return # Else, iterate through all child extensions... child_extensions = proto_utils.get_value_at_field(extension, 'extension') for child_extension in child_extensions: field = fields_by_url.get(child_extension.url.value) if field is None: raise ValueError(f'Message of type: {desc.full_name} has no field ' f'with name: {child_extension.url.value}.') # Simple value type on child_extension... if proto_utils.field_is_set(child_extension, 'value'): _add_extension_value_to_message(child_extension, msg, field) continue # Recurse for nested composite messages... if not proto_utils.field_is_repeated(field): if proto_utils.field_is_set(msg, field): raise ValueError( f'Field: {field.full_name} is already set on message: ' f'{desc.full_name}.') if proto_utils.field_content_length(child_extension, 'extension') > 1: raise ValueError( f'Cardinality mismatch between field: {field.full_name} and ' f'extension: {desc.full_name}.') child_message = proto_utils.set_in_parent_or_add(msg, field) add_extension_to_message(child_extension, child_message)
def _add_extension_value_to_message(extension: message.Message, msg: message.Message, message_field: descriptor.FieldDescriptor): """Serialize the provided extension and add it to the message. Args: extension: The FHIR extension to serialize. msg: The message to add the serialized extension to. message_field: The field on the message to set. Raises: InvalidFhirError: In the event that the field to be set is not a singular message type, or if the provided extension is not singular (has nested extensions). """ if message_field.type != descriptor.FieldDescriptor.TYPE_MESSAGE: raise fhir_errors.InvalidFhirError( f'{msg.DESCRIPTOR.full_name} is not a FHIR extension type.') extension_field = extension.DESCRIPTOR.fields_by_name['extension'] if proto_utils.field_content_length(extension, extension_field) > 0: raise fhir_errors.InvalidFhirError( 'No child extensions should be set on ' f'{extension.DESCRIPTOR.full_name}.') value_field = _get_populated_extension_value_field(extension) # If a choice type, need to assign the extension value to the correct field. if annotation_utils.is_choice_type_field(message_field): choice_message = proto_utils.get_value_at_field(msg, message_field) choice_descriptor = choice_message.DESCRIPTOR for choice_field in choice_descriptor.fields: if (value_field.message_type.full_name == choice_field.message_type.full_name): _add_extension_value_to_message(extension, choice_message, choice_field) return raise ValueError( f'No field on Choice Type {choice_descriptor.full_name} ' f'for extension {extension.DESCRIPTOR.full_name}.') # If the target message is a bound Code type, we need to convert the generic # Code field from the extension into the target typed Code. if annotation_utils.has_fhir_valueset_url(message_field.message_type): typed_code = proto_utils.set_in_parent_or_add(msg, message_field) codes.copy_code(cast(Any, extension).value.code, typed_code) return # If the target message is bound to a Coding type, we must convert the generic # Coding field from the extension into the target typed Coding. if fhir_types.is_type_or_profile_of_coding(message_field.message_type): typed_coding = proto_utils.set_in_parent_or_add(msg, message_field) codes.copy_coding(cast(Any, extension).value.coding, typed_coding) return # Value types must match if not proto_utils.are_same_message_type(value_field.message_type, message_field.message_type): raise ValueError( 'Missing expected value of type ' f'{message_field.message_type.full_name} in extension ' f'{extension.DESCRIPTOR.full_name}.') value = proto_utils.get_value_at_field( cast(Any, extension).value, value_field) if proto_utils.field_is_repeated(message_field): proto_utils.append_value_at_field(msg, message_field, value) else: proto_utils.set_value_at_field(msg, message_field, value)