Serialisierbare Basisklasse#

Dieses Rezept zeigt, wie eine serialisierbare Klasse auf TOML Fortran basierend konstruiert werden kann. Momentan ist TOML Fortran selbst keine Klasse als Basis für eine serialisierbare Klasse, daher werden wir eine Loader- und Dumper-Schnittstelle definieren, um eine Datei oder eine verbundene Unit in eine Datenstruktur zu konvertieren. Die abstrakte Basisklasse implementiert die Verarbeitung der Datei oder der Unit in eine TOML Datenstruktur und leitet diese an eine deferred Prozedur weiter, die in der Klasse implementiert wird um, ein Mapping von der TOML Datenstruktur and zurück zu definieren. Diese Art von Datenstruktur kann in verschiedenen Kontexten genutzt werden and einfach in TOML und zurück transferiert werden.

Bemerkung

TOML Fortran könnte in Zukunft selbst eine solche abstrakte Basisklasse bereitstellen.

Die Basisklasse kann wie folgt definiert werden

src/serde_class.f90#
!> Definition of configuration data with serde properties.
!> Each data record knows how to serialize and deserialize itself.
module serde_class
  use serde_error, only : error_type, fatal_error
  use tomlf, only : toml_table, toml_error, toml_load, toml_dump
  implicit none
  private

  public :: serde_record

  !> Serializable and deserializable configuration data record
  type, abstract :: serde_record
  contains
    !> Reading of configuration data
    generic :: load => load_from_file, load_from_unit, load_from_toml
    !> Read configuration data from file
    procedure, private :: load_from_file
    !> Read configuration data from formatted unit
    procedure, private :: load_from_unit
    !> Read configuration data from TOML data structure
    procedure(load_from_toml), deferred :: load_from_toml
    !> Writing of configuration data
    generic :: dump => dump_to_file, dump_to_unit, dump_to_toml
    !> Write configuration data to file
    procedure, private :: dump_to_file
    !> Write configuration data to formatted unit
    procedure, private :: dump_to_unit
    !> Write configuration data to TOML data structure
    procedure(dump_to_toml), deferred :: dump_to_toml
  end type serde_record

  abstract interface
    !> Read configuration data from TOML data structure
    subroutine load_from_toml(self, table, error)
      import :: serde_record, toml_table, error_type
      !> Instance of the configuration data
      class(serde_record), intent(inout) :: self
      !> Data structure
      type(toml_table), intent(inout) :: table
      !> Error handling
      type(error_type), allocatable, intent(out) :: error
    end subroutine load_from_toml
    !> Write configuration data to TOML datastructure
    subroutine dump_to_toml(self, table, error)
      import :: serde_record, toml_table, error_type
      !> Instance of the configuration data
      class(serde_record), intent(in) :: self
      !> Data structure
      type(toml_table), intent(inout) :: table
      !> Error handling
      type(error_type), allocatable, intent(out) :: error
    end subroutine dump_to_toml
  end interface

contains

  !> Read configuration data from file
  subroutine load_from_file(self, file, error)
    !> Instance of the configuration data
    class(serde_record), intent(inout) :: self
    !> File name
    character(len=*), intent(in) :: file
    !> Error handling
    type(error_type), allocatable, intent(out) :: error

    type(toml_error), allocatable :: parse_error
    type(toml_table), allocatable :: table

    call toml_load(table, file, error=parse_error)

    if (allocated(parse_error)) then
      allocate(error)
      call move_alloc(parse_error%message, error%message)
      return
    end if

    call self%load(table, error)
    if (allocated(error)) return
  end subroutine load_from_file

  !> Read configuration data from file
  subroutine load_from_unit(self, unit, error)
    !> Instance of the configuration data
    class(serde_record), intent(inout) :: self
    !> File name
    integer, intent(in) :: unit
    !> Error handling
    type(error_type), allocatable, intent(out) :: error

    type(toml_error), allocatable :: parse_error
    type(toml_table), allocatable :: table

    call toml_load(table, unit, error=parse_error)

    if (allocated(parse_error)) then
      allocate(error)
      call move_alloc(parse_error%message, error%message)
      return
    end if

    call self%load(table, error)
    if (allocated(error)) return
  end subroutine load_from_unit

  !> Write configuration data to file
  subroutine dump_to_file(self, file, error)
    !> Instance of the configuration data
    class(serde_record), intent(in) :: self
    !> File name
    character(len=*), intent(in) :: file
    !> Error handling
    type(error_type), allocatable, intent(out) :: error

    integer :: unit

    open(file=file, newunit=unit)
    call self%dump(unit, error)
    close(unit)
    if (allocated(error)) return

  end subroutine dump_to_file

  !> Write configuration data to file
  subroutine dump_to_unit(self, unit, error)
    !> Instance of the configuration data
    class(serde_record), intent(in) :: self
    !> Formatted unit
    integer, intent(in) :: unit
    !> Error handling
    type(error_type), allocatable, intent(out) :: error

    type(toml_table) :: table

    table = toml_table()

    call self%dump(table, error)

    if (.not.allocated(error)) then
      call toml_dump(table, unit, error)
    end if

  end subroutine dump_to_unit

end module serde_class

Wir definieren auch einen Fehler-Handler, der die Fehlermeldung speichert und den Fehlerstatus durch seinen Allokationstatus signalisiert.

src/serde_error.f90#
!> Central registry for error codes
module serde_error
  implicit none
  private

  public :: error_type, fatal_error

  !> Possible error codes
  type :: enum_stat
    !> Successful run
    integer :: success = 0
    !> Internal error:
    integer :: fatal = 1
  end type enum_stat

  !> Actual enumerator for return states
  type(enum_stat), parameter :: serde_stat = enum_stat()

  !> Error message
  type :: error_type
    !> Error code
    integer :: stat
    !> Payload of the error
    character(len=:), allocatable :: message
  end type error_type

contains

  !> A fatal error is encountered
  subroutine fatal_error(error, message, stat)
    !> Instance of the error
    type(error_type), allocatable, intent(out) :: error
    !> A detailed message describing the error and (optionally) offering advice
    character(len=*), intent(in), optional :: message
    !> Overwrite of the error code
    integer, intent(in), optional :: stat

    allocate(error)

    if (present(stat)) then
      error%stat = stat
    else
      error%stat = serde_stat%fatal
    end if

    if (present(message)) then
      error%message = message
    else
      error%message = "Fatal error"
    end if
  end subroutine fatal_error

end module serde_error

Ein Beispiel für eine serialisierbare Klasse basierend auf der oben gegebenen Basisklasse ist unten aufgeführt.

src/demo.f90#
module demo_serde
  use serde_class, only : serde_record
  use serde_error, only : error_type, fatal_error
  use tomlf, only : toml_table, get_value, set_value
  implicit none
  private

  public :: example_record

  type, extends(serde_record) :: example_record
    integer :: nrun
    real :: alpha
    character(len=:), allocatable :: label
  contains
    !> Read configuration data from TOML data structure
    procedure :: load_from_toml
    !> Write configuration data to TOML data structure
    procedure :: dump_to_toml
  end type example_record

contains

  !> Read configuration data from TOML data structure
  subroutine load_from_toml(self, table, error)
    !> Instance of the configuration data
    class(example_record), intent(inout) :: self
    !> Data structure
    type(toml_table), intent(inout) :: table
    !> Error handling
    type(error_type), allocatable, intent(out) :: error

    integer :: stat

    call get_value(table, "nrun", self%nrun, 10, stat=stat)
    if (stat /= 0) then
      call fatal_error(error, "Invalid entry for number of runs")
      return
    end if

    call get_value(table, "alpha", self%alpha, 1.0, stat=stat)
    if (stat /= 0) then
      call fatal_error(error, "Invalid entry for alpha parameter")
      return
    end if

    call get_value(table, "label", self%label, stat=stat)
    if (stat /= 0) then
      call fatal_error(error, "Invalid entry for data label")
      return
    end if
  end subroutine load_from_toml


  !> Write configuration data to TOML datastructure
  subroutine dump_to_toml(self, table, error)
    !> Instance of the configuration data
    class(example_record), intent(in) :: self
    !> Data structure
    type(toml_table), intent(inout) :: table
    !> Error handling
    type(error_type), allocatable, intent(out) :: error

    call set_value(table, "nrun", self%nrun)
    call set_value(table, "alpha", self%alpha)
    if (allocated(self%label)) then
      call set_value(table, "label", self%label)
    end if
  end subroutine dump_to_toml

end module demo_serde

Die definierte Datenklasse kann in einer Anwendung einfach aus einer Datei geladen werden, während die tatsächliche Implementierung sich nicht um das Lesen der TOML Datenstruktur kümmert, sondern kann sich auch darauf verlassen, dass wenn die Konfigurationsdatei gültiges TOML war, eine Datenstruktur zum Lesen bereitsteht.