def testSetInParentOrAdd_withSingularPrimitive_raises(self): """Test set_in_parent_or_add with singular proto primitive.""" boolean = datatypes_pb2.Boolean() with self.assertRaises(ValueError) as ve: proto_utils.set_in_parent_or_add(boolean, "value") self.assertIsInstance(ve.exception, ValueError)
def copy_coding(source: message.Message, target: message.Message): """Copies all fields from source to target "Coding" messages. Args: source: The FHIR coding instance to copy from. target: The FHIR coding instance to copy to. Raises: InvalidFhirError: In the event that source or target is not a type/profile of Coding. """ if not fhir_types.is_type_or_profile_of_coding(source.DESCRIPTOR): raise fhir_errors.InvalidFhirError( f'Source: {source.DESCRIPTOR.full_name} ' 'is not a type or profile of Coding.') if not fhir_types.is_type_or_profile_of_coding(target.DESCRIPTOR): raise fhir_errors.InvalidFhirError( f'Target: {target.DESCRIPTOR.full_name} ' 'is not a type or profile of Coding.') if proto_utils.are_same_message_type(source.DESCRIPTOR, target.DESCRIPTOR): target.CopyFrom(source) return # Copy fields present in both profiled and unprofiled codings. proto_utils.copy_common_field(source, target, 'id') proto_utils.copy_common_field(source, target, 'extension') proto_utils.copy_common_field(source, target, 'version') proto_utils.copy_common_field(source, target, 'display') proto_utils.copy_common_field(source, target, 'user_selected') # Copy the "code" field from source to target source_code = proto_utils.get_value_at_field(source, 'code') copy_code(source_code, proto_utils.set_in_parent_or_add(target, 'code')) target_system_field = target.DESCRIPTOR.fields_by_name.get('system') # TODO: This will fail if there is a target system field, # *and* a source system field, since in this case the source code will not # contain the system information, the containing Coding would. In general, # it's not quite right to get the system from Code, since unprofiled codes # don't contain system information. In practice, this isn't a problem, # because the only kind of profiled Codings we currently support are # Codings with typed Codes (which contain source information) but this is # not neccessary according to FHIR spec. if target_system_field is not None: source_system_str = get_system_for_code(source_code) target_system_uri = proto_utils.set_in_parent_or_add( target, target_system_field) proto_utils.set_value_at_field(target_system_uri, 'value', source_system_str)
def copy_coding(source: message.Message, target: message.Message): """Copies all fields from source to target "Coding" messages. Args: source: The FHIR coding instance to copy from. target: The FHIR coding instance to copy to. Raises: InvalidFhirError: In the event that source or target is not a type/profile of Coding. """ if not fhir_types.is_type_or_profile_of_coding(source.DESCRIPTOR): raise fhir_errors.InvalidFhirError( f'Source: {source.DESCRIPTOR.full_name} ' 'is not a type or profile of Coding.') if not fhir_types.is_type_or_profile_of_coding(target.DESCRIPTOR): raise fhir_errors.InvalidFhirError( f'Target: {target.DESCRIPTOR.full_name} ' 'is not a type or profile of Coding.') if proto_utils.are_same_message_type(source.DESCRIPTOR, target.DESCRIPTOR): target.CopyFrom(source) return # Copy fields present in both profiled and unprofiled codings. proto_utils.copy_common_field(source, target, 'id') proto_utils.copy_common_field(source, target, 'extension') proto_utils.copy_common_field(source, target, 'version') proto_utils.copy_common_field(source, target, 'display') proto_utils.copy_common_field(source, target, 'user_selected') # Copy the "code" field from source to target source_code = proto_utils.get_value_at_field(source, 'code') copy_code(source_code, proto_utils.set_in_parent_or_add(target, 'code')) target_system_field = target.DESCRIPTOR.fields_by_name.get('system') if target_system_field is not None: source_system_str = get_system_for_code(source_code) target_system_uri = proto_utils.set_in_parent_or_add( target, target_system_field) proto_utils.set_value_at_field(target_system_uri, 'value', source_system_str)
def testSetInParentOrAdd_withSingularComposite_returnsMessage(self): """Test set_in_parent_or_add with a singlular composite field.""" patient = patient_pb2.Patient() self.assertFalse(proto_utils.field_is_set(patient, "active")) active_set_in_parent = proto_utils.set_in_parent_or_add(patient, "active") self.assertTrue(proto_utils.field_is_set(patient, "active")) self.assertFalse(active_set_in_parent.value) self.assertFalse(patient.active.value) active_set_in_parent.value = True self.assertTrue(active_set_in_parent.value) self.assertTrue(patient.active.value)
def testSetInParentOrAdd_withRepeatedComposite_returnsMessage(self): """Test set_in_parent_or_add with repeated composite field.""" patient = patient_pb2.Patient() self.assertFalse(proto_utils.field_is_set(patient, "name")) name_set_in_parent = proto_utils.set_in_parent_or_add(patient, "name") self.assertTrue(proto_utils.field_is_set(patient, "name")) self.assertEmpty(name_set_in_parent.text.value) self.assertEmpty(patient.name[0].text.value) name_set_in_parent.text.value = "Foo" self.assertEqual(name_set_in_parent.text.value, "Foo") self.assertEqual(patient.name[0].text.value, "Foo")
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 populate_typed_reference_id(reference_id: message.Message, resource_id: str, version: Optional[str]): """Sets the resource_id and optionally, version, on the reference.""" reference_id_value_field = reference_id.DESCRIPTOR.fields_by_name['value'] proto_utils.set_value_at_field(reference_id, reference_id_value_field, resource_id) if version is not None: history_field = reference_id.DESCRIPTOR.fields_by_name.get('history') if history_field is None: raise ValueError('Not a valid ReferenceId message: ' f"{reference_id.DESCRIPTOR.full_name}. Field 'history' " 'does not exist.') history = proto_utils.set_in_parent_or_add(reference_id, history_field) history_value_field = history.DESCRIPTOR.fields_by_name['value'] proto_utils.set_value_at_field(history, history_value_field, version)
def _merge_contained_resource(self, json_value: Dict[str, Any], target: message.Message): """Merges json_value into a contained resource field within target.""" # We handle contained resources in a special way, because despite internally # being a oneof, it is not actually a chosen-type in FHIR. The JSON field # name is just 'resource', which doesn't give any clues about which field in # the oneof to set. Instead, we need to inspect the JSON input to determine # its type. Then, merge into that specific fields in the resource oneof. resource_type = json_value.get('resourceType') contained_field = self._resource_type_mapping.get(resource_type) if contained_field is None: raise ValueError('Unable to retrieve ContainedResource field for ' f'{resource_type}.') contained_message = proto_utils.set_in_parent_or_add( target, contained_field) self._merge_message(json_value, contained_message)
def _merge_choice_field(self, json_value: Any, choice_field: descriptor.FieldDescriptor, field_name: str, parent: message.Message): """Creates a Message based on the choice_field Descriptor and json_value. The resulting message is merged into parent. Args: json_value: The JSON value to merge into a message of the type described by choice_field. choice_field: The field descriptor of the FHIR choice type on parent. field_name: The nested field name of the choice type, e.g.: _valueBoolean. parent: The parent Message to merge into. """ choice_field_name = _get_choice_field_name(choice_field, field_name) choice_field_map = _get_field_map(choice_field.message_type) choice_value_field = choice_field_map.get(choice_field_name) if choice_value_field is None: raise ValueError(f'Cannot find {choice_field_name!r} on ' f'{choice_field.full_name}') choice_message = proto_utils.set_in_parent_or_add(parent, choice_field) self._merge_field(json_value, choice_value_field, choice_message)
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)