Exemplo n.º 1
0
def get_system_for_code(code: message.Message) -> str:
    """Returns the code system associated with the provided Code."""
    system_field = code.DESCRIPTOR.fields_by_name.get('system')
    if system_field is not None:
        return proto_utils.get_value_at_field(code, system_field)

    fixed_coding_system = annotation_utils.get_fixed_coding_system(code)
    if fixed_coding_system is not None:
        # The entire profiled coding can only be from a single system. Use that.
        return fixed_coding_system

    # There is no single system for the whole coding. Look for the coding system
    # annotation on the enum.
    enum_field = code.DESCRIPTOR.fields_by_name.get('value')
    if (enum_field is None
            or enum_field.type != descriptor.FieldDescriptor.TYPE_ENUM):
        raise fhir_errors.InvalidFhirError(
            f'Invalid profiled Coding: {code.DESCRIPTOR.full_name}; missing system '
            'information on string code.')

    enum_value = proto_utils.get_value_at_field(code, enum_field)
    enum_value_descriptor = enum_field.enum_type.values_by_number[enum_value]
    if not annotation_utils.has_source_code_system(enum_value_descriptor):
        raise fhir_errors.InvalidFhirError(
            f'Invalid profiled Coding: {code.DESCRIPTOR.full_name}; missing system '
            'information on enum code')
    return cast(str,
                annotation_utils.get_source_code_system(enum_value_descriptor))
Exemplo n.º 2
0
    def _nonnull_string_value(self) -> str:
        precision: int = proto_utils.get_value_at_field(
            self.wrapped, 'precision')
        f = _FORMAT_FUNCS.get(precision)
        if f is None:
            raise fhir_errors.InvalidFhirError(
                f'No format string for precision: {precision!r}.')

        # The wrapped value represents the time (based on a 24-hour clock) in micro-
        # seconds since 00:00:00.000000. In order to convert this to a datetime.time
        # object for formatting, we need to use a datetime.timedelta to compute the
        # time "offset" represented by self.wrapped.
        #
        # A timedelta object can only be added to a datetime. Note that the base
        # datetime created by datetime.combine() has no associated tzinfo (e.g. it
        # is "naive"), and therefore concepts like DST, timezones, etc. will not
        # affect this calculation.
        base_time = datetime.datetime.combine(_primitive_time_utils.UNIX_EPOCH,
                                              datetime.time())
        value_us: int = proto_utils.get_value_at_field(self.wrapped,
                                                       'value_us')
        delta = datetime.timedelta(microseconds=value_us)
        if delta.days > 0:
            raise fhir_errors.InvalidFhirError(
                f'Time value of {value_us!r}us is >= 24 hours.')

        time_value = (base_time + delta).time()
        return f(time_value)
def validate_primitive_without_value(fhir_primitive: message.Message):
    """Validates a Message which has the PrimitiveWithoutValue extension.

  Given that there is a PrimitiveWithoutValue extension present, there must be
  at least one other extension. Otherwise, there is truly no value set other
  than id and/or extension (non-value fields).

  Args:
    fhir_primitive: The FHIR primitive Message to validate.

  Raises:
    fhir_errors.InvalidFhirError: In the event that there is less than one
    extension present, or there are values set other than id and/or extension.
  """
    name = fhir_primitive.DESCRIPTOR.full_name

    # There must be at least one other FHIR extension
    if len(extensions.get_fhir_extensions(fhir_primitive)) < 2:
        raise fhir_errors.InvalidFhirError(
            f'{name!r} must have either extensions or a value present.')

    # ... Else truly no value set other than id and/or extension
    for field, _ in fhir_primitive.ListFields():  # Iterate set fields only
        if field.name not in extensions.NON_VALUE_FIELDS:
            raise fhir_errors.InvalidFhirError(
                f'{name!r} contains PrimitiveHasNoValue but {field.name!r} is set.'
            )
Exemplo n.º 4
0
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}`.')
Exemplo n.º 5
0
  def _nonnull_string_value(self) -> str:
    timezone: str = proto_utils.get_value_at_field(self.wrapped, 'timezone')
    if not timezone:
      raise fhir_errors.InvalidFhirError('DateTime missing timezone.')

    precision: int = proto_utils.get_value_at_field(self.wrapped, 'precision')
    f = _FORMAT_FUNCS.get(precision)
    if f is None:
      raise fhir_errors.InvalidFhirError('Invalid Precision on DateTime')

    dt_str = f(_primitive_time_utils.get_date_time_value(self.wrapped))
    return _primitive_time_utils.restore_utc_timezone(dt_str, timezone)
Exemplo n.º 6
0
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)
Exemplo n.º 7
0
def _validate_period(period: message.Message, base_name: str):
    """Validates that a timelike period has a valid start and end value."""
    if not (proto_utils.field_is_set(period, 'start')
            and proto_utils.field_is_set(period, 'end')):
        return  # Early exit; either start or end field is not present

    # Check whether start time is greater than end time. Note that, if it is,
    # that's not necessarily invalid, since the precisions can be different. So we
    # need to compare the end time at the upper bound of the end element.
    #
    # Example: If the start time is "Tuesday at noon", and the end time is "some
    # time Tuesday", this is valid even though the timestamp used for "some time
    # Tuesday" is Tuesday 00:00, since the precision for the start is higher than
    # the end.
    #
    # Also note the GetUpperBoundFromTimelikeElement is always greater than the
    # time itself by exactly one time unit, and hence start needs to be strictly
    # less than the upper bound of end, so as not to allow ranges like [Tuesday,
    # Monday] to be valid.
    start: message.Message = proto_utils.get_value_at_field(period, 'start')
    end: message.Message = proto_utils.get_value_at_field(period, 'end')
    end_precision = proto_utils.get_value_at_field(end, 'precision')

    start_dt_value = _primitive_time_utils.get_date_time_value(start)
    end_upper_bound = _primitive_time_utils.get_upper_bound(
        end, _primitive_time_utils.DateTimePrecision(end_precision))
    if start_dt_value >= end_upper_bound:
        raise fhir_errors.InvalidFhirError(
            f'`{base_name}` start time is later than end time.')
Exemplo n.º 8
0
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 from_json_value(cls, json_value: Optional[Any],
                        primitive_cls: Type[message.Message],
                        context: Context) -> 'PrimitiveWrapper':
        """Parses json_value into an instance of primitive_cls and wraps.

    Args:
      json_value: The optional raw json_value to parse and wrap.
      primitive_cls: The type of FHIR primitive message to create and validate.
      context: Related primitive information to use for printing/parsing a
        wrapped primitive.

    Returns:
      An instance of PrimitiveWrapper.
    """
        if isinstance(json_value, (list, tuple)):
            raise ValueError(f'Error, unable to wrap sequence: {json_value}.')

        if json_value is None or isinstance(json_value, (dict, )):
            return cls(no_value_primitive(primitive_cls), context)

        if not isinstance(json_value,
                          cast(Tuple[Type[Any], ...], cls._PARSABLE_TYPES)):
            raise fhir_errors.InvalidFhirError(
                f'Unable to parse {json_value!r}. {type(json_value)} is invalid.'
            )

        return cls.from_json_str(str(json_value), primitive_cls, context)
Exemplo n.º 10
0
  def _nonnull_string_value(self) -> str:
    timezone: str = proto_utils.get_value_at_field(self.wrapped, 'timezone')
    if not timezone:
      raise fhir_errors.InvalidFhirError('Date missing timezone.')

    precision: int = proto_utils.get_value_at_field(self.wrapped, 'precision')
    f = _FORMAT_FUNCS.get(precision)
    if f is None:
      raise fhir_errors.InvalidFhirError(
          'No format string for precision: '
          f'{_primitive_time_utils.DateTimePrecision(precision)!r}.')

    tzinfo = _primitive_time_utils.timezone_info_for_timezone(timezone)
    delta = datetime.timedelta(
        microseconds=cast(
            int, proto_utils.get_value_at_field(self.wrapped, 'value_us')))
    dt_value = (_primitive_time_utils.UNIX_EPOCH + delta).astimezone(tzinfo)
    return f(dt_value)
Exemplo n.º 11
0
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)
Exemplo n.º 12
0
def _parse(json_str: str, primitive_cls: Type[Base64Binary], *,
           separator_stride_cls: Type[SeparatorStride]) -> Base64Binary:
    """Parses the json_str into a Base64Binary FHIR primitive protobuf message.

  Args:
    json_str: The raw JSON string to parse.
    primitive_cls: The type of FHIR primitive to parse into.
    separator_stride_cls: The type of Base64BinarySeparatorStride extension
      associated with primitive_cls.

  Returns:
    A FHIR primitive Base64Binary protobuf message.

  Raises:
    fhir_errors.InvalidFhirError: In the event that the provided json_str is
    not a valid base64-encoded string.
  """
    # Properly capture the FHIR-allowed white-space separator, if one exists.
    # This is a series of one or more spaces separating valid base64 encoded data.
    # This series is repeated at the same intervals throughout the entirety of the
    # json_str.
    #
    # For example, for a json_str of 'Zm9v  YmFy', this would result in:
    # json_str: 'Zm9v  YmFy'
    # separator: '  '
    # stride: 4
    result = primitive_cls()
    stride = json_str.find(' ')
    if stride != -1:
        end = stride
        while end < len(json_str) and json_str[end] == ' ':
            end += 1
        separator = json_str[stride:end]

        # Silencing the type checkers as pytype doesn't fully support structural
        # subtyping yet.
        # pylint: disable=line-too-long
        # See: https://mypy.readthedocs.io/en/stable/casts.html#casts-and-type-assertions.
        # pylint: enable=line-too-long
        # Soon: https://www.python.org/dev/peps/pep-0544/.
        separator_stride_extension = cast(Any, separator_stride_cls())
        separator_stride_extension.separator.value = separator
        separator_stride_extension.stride.value = stride
        extensions.add_message_to_extension(separator_stride_extension,
                                            result.extension.add())

        json_str = json_str.replace(separator, '')

    try:
        result.value = base64.b64decode(json_str, validate=True)
    except binascii.Error as e:
        raise fhir_errors.InvalidFhirError(
            'Invalid base64-encoded string.') from e
    return result
Exemplo n.º 13
0
    def _nonnull_string_value(self) -> str:
        timezone: str = proto_utils.get_value_at_field(self.wrapped,
                                                       'timezone')
        if not timezone:
            raise fhir_errors.InvalidFhirError('Instant missing timezone.')

        precision: int = proto_utils.get_value_at_field(
            self.wrapped, 'precision')
        f = _FORMAT_FUNCS.get(precision)
        if f is None:
            raise fhir_errors.InvalidFhirError('Invalid precision on Instant.')
        value_us: int = proto_utils.get_value_at_field(self.wrapped,
                                                       'value_us')
        tzinfo = _primitive_time_utils.timezone_info_for_timezone(timezone)
        delta = datetime.timedelta(microseconds=value_us)
        datetime_value = (_primitive_time_utils.UNIX_EPOCH +
                          delta).astimezone(tzinfo)

        datetime_str = f(datetime_value)
        return _primitive_time_utils.restore_utc_timezone(
            datetime_str, timezone)
Exemplo n.º 14
0
  def from_json_value(cls, json_value: Optional[Any],
                      primitive_cls: Type[message.Message],
                      context: Context) -> 'XhtmlWrapper':
    """See PrimitiveWrapper.from_json_value."""
    if json_value is None or isinstance(json_value, (dict,)):
      raise ValueError(
          f'Invalid input for class: {primitive_cls!r}.')  # Disallow None

    if not isinstance(json_value, cast(Tuple[Type[Any], ...],
                                       cls.PARSABLE_TYPES)):
      raise fhir_errors.InvalidFhirError(
          f'Unable to parse Xhtml. {type(json_value)} is invalid.')

    return cast(XhtmlWrapper,
                cls.from_json_str(str(json_value), primitive_cls, context))
Exemplo n.º 15
0
def code_string_to_enum_value_descriptor(
    code_string: str, enum_descriptor: descriptor.EnumDescriptor
) -> descriptor.EnumValueDescriptor:
    """Returns an EnumValueDescriptor for a provided EnumDescriptor and raw code.

  Args:
    code_string: A raw string representation of the code to retrieve.
    enum_descriptor: The EnumDescriptor the desired EnumValueDescriptor belongs
      to.

  Returns:
    An instance of EnumValueDescriptor that the code_string represents.

  Raises:
    fhir_errors.InvalidFhirError: In the event that a conversion from
    code_string was unsuccessful.
  """
    # Check the shared memos mapping
    value_descriptor = _get_enum_value_descriptor_memo(enum_descriptor,
                                                       code_string)
    if value_descriptor is not None:
        return value_descriptor

    # Make minor substitutions to the raw value, and search for value descriptor.
    # If found, update the shared memo mapping.
    fhir_case_code_string = code_string.upper().replace('-', '_')
    value_descriptor = enum_descriptor.values_by_name.get(
        fhir_case_code_string)
    if value_descriptor is not None:
        _set_enum_value_descriptor_memo(enum_descriptor, code_string,
                                        value_descriptor)
        return value_descriptor

    # Finally, some codes had to be renamed to make them valid enum values.
    # Iterate through all target enum values, and look for the FHIR original code
    # extension value.
    for value_descriptor in enum_descriptor.values:
        if (value_descriptor.GetOptions().HasExtension(
                annotations_pb2.fhir_original_code)
                and value_descriptor.GetOptions().Extensions[
                    annotations_pb2.fhir_original_code] == code_string):
            _set_enum_value_descriptor_memo(enum_descriptor, code_string,
                                            value_descriptor)
            return value_descriptor

    raise fhir_errors.InvalidFhirError(
        f'Failed to convert {code_string!r} to {enum_descriptor.full_name}. No '
        f'matching enum found.')
Exemplo n.º 16
0
def validate_primitive_json_representation(desc: descriptor.Descriptor,
                                           json_str: str) -> None:
  """Ensures that json_str matches the associated regex pattern, if one exists.

  Args:
    desc: The Descriptor of the FHIR primitive to validate.
    json_str: The JSON string to validate.

  Raises:
    fhir_errors.InvalidFhirError: Raised in the event that pattern is unable to
    be matched on json_str.
  """
  pattern = _pattern_for_primitive(desc)

  # Raise an exception if we're unable to detect the pattern
  if pattern is not None and pattern.fullmatch(json_str) is None:
    raise fhir_errors.InvalidFhirError(f'Unable to find pattern: {pattern!r}.')
Exemplo n.º 17
0
def _add_value_to_extension(msg: message.Message, extension: message.Message,
                            is_choice_type: bool):
    """Adds the fields from msg to a generic Extension.

  Attempts are first made to set the "value" field of the generic Extension
  based on the type of field set on message. If this fails, checks are made
  against the generic Code and Coding types, and finally we fall back to adding
  the message's fields as sub-extensions.

  Args:
    msg: The message whose values to add to extension.
    extension: The generic Extension to populate.
    is_choice_type: Whether or not the provided message represents a "choice"
    type.
  """
    if is_choice_type:
        oneofs = msg.DESCRIPTOR.oneofs
        if not oneofs:
            raise fhir_errors.InvalidFhirError(
                f'Choice type is missing a oneof: {msg.DESCRIPTOR.full_name}')
        value_field_name = msg.WhichOneof(oneofs[0].name)
        if value_field_name is None:
            raise ValueError('Choice type has no value set: '
                             f'{msg.DESCRIPTOR.full_name}')
        value_field = msg.DESCRIPTOR.fields_by_name[value_field_name]
        _verify_field_is_proto_message_type(value_field)
        _add_value_to_extension(
            proto_utils.get_value_at_field(msg, value_field), extension, False)
    else:
        # Try to set the message directly as a datatype value on the extension.
        # E.g., put the message of type Boolean into the value.boolean field
        value_field_mapping = _get_value_field_mapping_for_extension(extension)
        value_field = value_field_mapping.get(msg.DESCRIPTOR.full_name)
        if value_field is not None:
            proto_utils.set_value_at_field(
                cast(Any, extension).value, value_field, msg)
        elif annotation_utils.has_fhir_valueset_url(msg):
            codes.copy_code(msg, cast(Any, extension).value.code)
        elif fhir_types.is_type_or_profile_of_coding(msg):
            codes.copy_coding(msg, cast(Any, extension).value.coding)
        else:  # Fall back to adding individual fields as sub-extensions
            _add_fields_to_extension(msg, extension)
Exemplo n.º 18
0
def _parse(json_str: str, primitive_cls: Type[Date], *,
           default_timezone: str) -> Date:
  """Parses the json_str into a Date FHIR primitive.

  Args:
    json_str: The raw JSON string to parse.
    primitive_cls: The FHIR primitive to parse into.
    default_timezone: The default timezone to use when parsing in the event that
      no timezone information is present.

  Returns:
    A FHIR primitive Date.

  Raises:
    fhir_errors.InvalidFhirError: In the event that no datetime format was
    able to properly parse the json_str.
  """
  try:
    dt = datetime.datetime.strptime(json_str, '%Y')
    return _primitive_time_utils.build_date_like(
        dt, default_timezone, _primitive_time_utils.DateTimePrecision.YEAR,
        primitive_cls)
  except ValueError:
    pass  # Fall through

  try:
    dt = datetime.datetime.strptime(json_str, '%Y-%m')
    return _primitive_time_utils.build_date_like(
        dt, default_timezone, _primitive_time_utils.DateTimePrecision.MONTH,
        primitive_cls)
  except ValueError:
    pass  # Fall through

  try:
    dt = datetime.datetime.strptime(json_str, '%Y-%m-%d')
    return _primitive_time_utils.build_date_like(
        dt, default_timezone, _primitive_time_utils.DateTimePrecision.DAY,
        primitive_cls)
  except ValueError:
    pass  # Fall through

  raise fhir_errors.InvalidFhirError(f'Invalid Date: {json_str!r}.')
Exemplo n.º 19
0
def _validate_fhir_constraints(
        msg: message.Message, base_name: str,
        primitive_handler_: primitive_handler.PrimitiveHandler):
    """Iterates over fields of the provided message and validates constraints.

  Args:
    msg: The message to validate.
    base_name: The root message name for recursive validation of nested message
      fields.
    primitive_handler_: Responsible for returning PrimitiveWrappers.

  Raises:
    fhir_errors.InvalidFhirError: In the event that a field is found to be
    violating FHIR constraints or a required oneof is not set.
  """
    if annotation_utils.is_primitive_type(msg):
        # Validation is implicitly done on the primitive type during wrapping
        _ = primitive_handler_.primitive_wrapper_from_primitive(msg)
        return

    if proto_utils.is_message_type(msg, any_pb2.Any):
        # We do not validate "Any" constrained resources.
        # TODO: Potentially unpack the correct type and validate?
        return

    # Enumerate and validate fields of the message
    for field in msg.DESCRIPTOR.fields:
        field_name = f'{base_name}.{field.json_name}'
        _validate_field(msg, field, field_name, primitive_handler_)

    # Also verify that oneof fields are set. Note that optional choice-types
    # should have the containing message unset - if the containing message is set,
    # it should have a value set as well
    for oneof in msg.DESCRIPTOR.oneofs:
        if (msg.WhichOneof(oneof.name) is None
                and not oneof.GetOptions().HasExtension(
                    annotations_pb2.fhir_oneof_is_optional)):
            raise fhir_errors.InvalidFhirError(
                f'Empty oneof: `{oneof.full_name}`.')
Exemplo n.º 20
0
def _parse(json_str: str, primitive_cls: Type[Instant]) -> Instant:
    """Parses the json_str into an Instant FHIR primitive.

  Args:
    json_str: The raw JSON string to parse.
    primitive_cls: The FHIR primitive to parse into.

  Returns:
    A FHIR primitive Instant.

  Raises:
    fhir_errors.InvalidFhirError: In the event that no FHIR primitive Instant
    format was able to properly parse the json_str.
  """
    datetime_str, timezone_str = _primitive_time_utils.split_timezone(json_str)
    try:
        dt = datetime.datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S')
        return _primitive_time_utils.build_date_like(
            dt, timezone_str, _primitive_time_utils.TimePrecision.SECOND,
            primitive_cls)
    except ValueError:
        pass  # Fall through

    try:
        dt = datetime.datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S.%f')
        if (_primitive_time_utils.PRECISION_PATTERN_MILLISECOND.search(
                datetime_str) is not None):
            return _primitive_time_utils.build_date_like(
                dt, timezone_str,
                _primitive_time_utils.TimePrecision.MILLISECOND, primitive_cls)
        elif (_primitive_time_utils.PRECISION_PATTERN_MICROSECOND.search(
                datetime_str) is not None):
            return _primitive_time_utils.build_date_like(
                dt, timezone_str,
                _primitive_time_utils.TimePrecision.MICROSECOND, primitive_cls)
    except ValueError:
        pass  # Fall through

    raise fhir_errors.InvalidFhirError(f'Invalid Instant: {json_str!r}.')
Exemplo n.º 21
0
def _parse(json_str: str, primitive_cls: Type[Time]) -> Time:
    """Parses the json_str into a Time FHIR primitive.

  Args:
    json_str: The raw JSON string to parse.
    primitive_cls: The FHIR primitive to parse into.

  Returns:
    A FHIR primitive Time instance.

  Raises:
    fhir_errors.InvalidFhirError: In the event that no FHIR primitive Time
    format was able to properly parse the json_str.
  """
    try:
        time = datetime.datetime.strptime(json_str, '%H:%M:%S').time()
        return _primitive_time_utils.build_time(
            time, _primitive_time_utils.TimePrecision.MICROSECOND.SECOND,
            primitive_cls)
    except ValueError:
        pass  # Fall through

    try:
        time = datetime.datetime.strptime(json_str, '%H:%M:%S.%f').time()
        if (_primitive_time_utils.PRECISION_PATTERN_MILLISECOND.search(
                json_str) is not None):
            return _primitive_time_utils.build_time(
                time, _primitive_time_utils.TimePrecision.MILLISECOND,
                primitive_cls)
        elif (_primitive_time_utils.PRECISION_PATTERN_MICROSECOND.search(
                json_str) is not None):
            return _primitive_time_utils.build_time(
                time, _primitive_time_utils.TimePrecision.MICROSECOND,
                primitive_cls)
    except ValueError:
        pass  # Fall through

    raise fhir_errors.InvalidFhirError(f'Invalid Time: {json_str!r}.')
Exemplo n.º 22
0
def _get_value_field_mapping_for_extension(
        extension: message.Message) -> Dict[str, message.Message]:
    """Returns a mapping for each possible value of the extension.value field.

  The mapping is from the full field name of each possible value in the
  Extension.ValueX oneof field to its corresponding FieldDescriptor.

  Args:
    extension: The extension to examine and return a mapping for.

  Returns:
    A mapping from the Extension.ValueX oneof fields' full names to the
    associated FieldDescriptors.

  Raises:
    fhir_errors.InvalidFhirError: In the event that the provided extension
    isn't a valid FHIR extension.
  """
    with _value_field_map_cv:
        if extension.DESCRIPTOR.full_name in _value_field_map:
            return _value_field_map[extension.DESCRIPTOR.full_name]

    value_field = extension.DESCRIPTOR.fields_by_name.get('value')
    if not value_field:
        raise fhir_errors.InvalidFhirError(
            f'Extension: {extension.DESCRIPTOR.full_name} has no "value" field.'
        )

    # Build mapping
    value_field_mapping = {
        field.message_type.full_name: field
        for field in value_field.message_type.fields
    }

    # Cache and return
    with _value_field_map_cv:
        _value_field_map[extension.DESCRIPTOR.full_name] = value_field_mapping
        return _value_field_map[extension.DESCRIPTOR.full_name]
Exemplo n.º 23
0
def get_code_as_string(code: message.Message) -> str:
    """Returns the string representation of a FHIR code."""
    if not fhir_types.is_type_or_profile_of_code(code):
        raise ValueError(
            f'Invalid type for get_code_as_string: {code.DESCRIPTOR.full_name}'
        )

    value_field = code.DESCRIPTOR.fields_by_name.get('value')
    if value_field is None:
        raise ValueError(
            f'Invalid code type for get_code_as_string: {code.DESCRIPTOR.full_name}'
        )

    value = proto_utils.get_value_at_field(code, value_field)
    if value_field.type == descriptor.FieldDescriptor.TYPE_STRING:
        return value
    elif value_field.type == descriptor.FieldDescriptor.TYPE_ENUM:
        return enum_value_descriptor_to_code_string(
            value_field.enum_type.values_by_number[value])
    else:
        raise fhir_errors.InvalidFhirError(
            f'Invalid value field type: {value_field.type!r} for code: '
            f'{code.DESCRIPTOR.full_name}')
Exemplo n.º 24
0
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)
Exemplo n.º 25
0
def _parse(json_str: str, primitive_cls: Type[DateTime], *,
           default_timezone: str) -> DateTime:
  """Parses the json_str into a DateTime FHIR primitive.

  Args:
    json_str: The raw JSON string to parse.
    primitive_cls: The FHIR primitive to parse into.
    default_timezone: The default timezone to use when parsing in the event that
      no timezone information is present.

  Returns:
    A FHIR primitive DateTime.

  Raises:
    fhir_errors.InvalidFhirError: In the event that no FHIR primitive DateTime
    format was able to properly parse the json_str.
  """
  try:
    dt = datetime.datetime.strptime(json_str, '%Y')
    return _primitive_time_utils.build_date_like(
        dt, default_timezone, _primitive_time_utils.DateTimePrecision.YEAR,
        primitive_cls)
  except ValueError:
    pass  # Fall through

  try:
    dt = datetime.datetime.strptime(json_str, '%Y-%m')
    return _primitive_time_utils.build_date_like(
        dt, default_timezone, _primitive_time_utils.DateTimePrecision.MONTH,
        primitive_cls)
  except ValueError:
    pass  # Fall through

  try:
    dt = datetime.datetime.strptime(json_str, '%Y-%m-%d')
    return _primitive_time_utils.build_date_like(
        dt, default_timezone, _primitive_time_utils.DateTimePrecision.DAY,
        primitive_cls)
  except ValueError:
    pass  # Fall through

  # Attempt to parse DateTime with provided time and timezone offset...
  datetime_str, timezone_str = _primitive_time_utils.split_timezone(json_str)
  try:
    dt = datetime.datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S')
    return _primitive_time_utils.build_date_like(
        dt, timezone_str, _primitive_time_utils.DateTimePrecision.SECOND,
        primitive_cls)
  except ValueError:
    pass  # Fall through

  try:
    dt = datetime.datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S.%f')
    if (_primitive_time_utils.PRECISION_PATTERN_MILLISECOND.search(datetime_str)
        is not None):
      return _primitive_time_utils.build_date_like(
          dt, timezone_str, _primitive_time_utils.DateTimePrecision.MILLISECOND,
          primitive_cls)
    elif (
        _primitive_time_utils.PRECISION_PATTERN_MICROSECOND.search(datetime_str)
        is not None):
      return _primitive_time_utils.build_date_like(
          dt, timezone_str, _primitive_time_utils.DateTimePrecision.MICROSECOND,
          primitive_cls)
  except ValueError:
    pass  # Fall through

  raise fhir_errors.InvalidFhirError(f'Invalid DateTime: {json_str!r}.')
Exemplo n.º 26
0
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)
Exemplo n.º 27
0
def copy_code(source: message.Message, target: message.Message):
    """Adds all fields from source to target.

  Args:
    source: The FHIR Code instance to copy from.
    target: The target FHIR Code instance to copy to.
  """
    if not fhir_types.is_type_or_profile_of_code(source.DESCRIPTOR):
        raise fhir_errors.InvalidFhirError(
            f'Source: {source.DESCRIPTOR.full_name} '
            'is not type or profile of Code.')

    if not fhir_types.is_type_or_profile_of_code(target.DESCRIPTOR):
        raise fhir_errors.InvalidFhirError(
            f'Target: {target.DESCRIPTOR.full_name} '
            'is not type or profile of Code.')

    if proto_utils.are_same_message_type(source.DESCRIPTOR, target.DESCRIPTOR):
        target.CopyFrom(source)
        return

    source_value_field = source.DESCRIPTOR.fields_by_name.get('value')
    target_value_field = target.DESCRIPTOR.fields_by_name.get('value')
    if source_value_field is None or target_value_field is None:
        raise fhir_errors.InvalidFhirError(
            'Unable to copy code from '
            f'{source.DESCRIPTOR.full_name} '
            f'to {target.DESCRIPTOR.full_name}.')

    proto_utils.copy_common_field(source, target, 'id')
    proto_utils.copy_common_field(source, target, 'extension')

    # Handle specialized codes
    if (source_value_field.type not in _CODE_TYPES
            or target_value_field.type not in _CODE_TYPES):
        raise ValueError(
            f'Unable to copy from {source.DESCRIPTOR.full_name} '
            f'to {target.DESCRIPTOR.full_name}. Must have a field '
            'of TYPE_ENUM or TYPE_STRING.')

    source_value = proto_utils.get_value_at_field(source, source_value_field)
    if source_value_field.type == target_value_field.type:
        # Perform a simple assignment if value_field types are equivalent
        proto_utils.set_value_at_field(target, target_value_field,
                                       source_value)
    else:
        # Otherwise, we need to transform the value prior to assignment...
        if source_value_field.type == descriptor.FieldDescriptor.TYPE_STRING:
            source_enum_value = code_string_to_enum_value_descriptor(
                source_value, target_value_field.enum_type)
            proto_utils.set_value_at_field(target, target_value_field,
                                           source_enum_value.number)
        elif source_value_field.type == descriptor.FieldDescriptor.TYPE_ENUM:
            source_string_value = enum_value_descriptor_to_code_string(
                source_value_field.enum_type.values_by_number[source_value])
            proto_utils.set_value_at_field(target, target_value_field,
                                           source_string_value)
        else:  # Should never hit
            raise ValueError('Unexpected generic value field type: '
                             f'{source_value_field.type}. Must be a field of '
                             'TYPE_ENUM or TYPE_STRING in order to copy.')