LCOV - code coverage report
Current view: top level - src/api - hsd_validation.f90 (source / functions) Coverage Total Hit
Test: coverage.info Lines: 91.9 % 99 91
Test Date: 2026-02-04 13:26:36 Functions: 100.0 % 5 5

            Line data    Source code
       1              : !> HSD validation and verification
       2              : !>
       3              : !> This module provides utilities for validating HSD data, including
       4              : !> required field checking, range validation, and unit conversion support.
       5              : module hsd_validation
       6              :   use hsd_constants, only: dp
       7              :   use hsd_utils, only: to_lower
       8              :   use hsd_error, only: hsd_error_t, make_error, HSD_STAT_OK, HSD_STAT_NOT_FOUND, &
       9              :     HSD_STAT_TYPE_ERROR
      10              :   use hsd_types, only: hsd_node, hsd_table, hsd_value, &
      11              :     VALUE_TYPE_NONE, VALUE_TYPE_STRING, VALUE_TYPE_INTEGER, &
      12              :     VALUE_TYPE_REAL, VALUE_TYPE_LOGICAL, VALUE_TYPE_ARRAY, VALUE_TYPE_COMPLEX
      13              :   use hsd_query, only: hsd_get_child
      14              :   implicit none (type, external)
      15              :   private
      16              : 
      17              :   ! Public procedures
      18              :   public :: hsd_require
      19              :   public :: hsd_validate_range
      20              :   public :: hsd_validate_one_of
      21              :   public :: hsd_get_with_unit
      22              : 
      23              : contains
      24              : 
      25              :   !> Require that a path exists and optionally check its type
      26              :   !>
      27              :   !> If the path doesn't exist or type doesn't match, creates a descriptive error.
      28           13 :   subroutine hsd_require(table, path, error, expected_type, context)
      29              :     type(hsd_table), intent(in), target :: table
      30              :     character(len=*), intent(in) :: path
      31              :     type(hsd_error_t), allocatable, intent(out) :: error
      32              :     integer, intent(in), optional :: expected_type
      33              :     character(len=*), intent(in), optional :: context
      34              : 
      35              :     class(hsd_node), pointer :: child
      36           13 :     integer :: local_stat, actual_type
      37           13 :     character(len=:), allocatable :: ctx_prefix
      38              : 
      39           13 :     call hsd_get_child(table, path, child, local_stat)
      40              : 
      41           13 :     if (present(context)) then
      42            2 :       ctx_prefix = context // ": "
      43              :     else
      44           11 :       ctx_prefix = ""
      45              :     end if
      46              : 
      47           13 :     if (local_stat /= 0 .or. .not. associated(child)) then
      48              :       call make_error(error, HSD_STAT_NOT_FOUND, &
      49            5 :         ctx_prefix // "Required field '" // path // "' not found")
      50            5 :       return
      51              :     end if
      52              : 
      53            8 :     if (present(expected_type)) then
      54              :       select type (child)
      55              :       type is (hsd_value)
      56            4 :         actual_type = child%value_type
      57            4 :         if (expected_type /= actual_type) then
      58              :           call make_error(error, HSD_STAT_TYPE_ERROR, &
      59              :             ctx_prefix // "Field '" // path // "' has wrong type: expected " // &
      60            2 :             type_name(expected_type) // ", got " // type_name(actual_type))
      61              :         end if
      62              :       type is (hsd_table)
      63            1 :         if (expected_type /= VALUE_TYPE_NONE) then
      64              :           call make_error(error, HSD_STAT_TYPE_ERROR, &
      65              :             ctx_prefix // "Field '" // path // "' is a table, expected value of type " // &
      66            1 :             type_name(expected_type))
      67              :         end if
      68              :       end select
      69              :     end if
      70              : 
      71           13 :   end subroutine hsd_require
      72              : 
      73              :   !> Get a human-readable name for a value type
      74            5 :   pure function type_name(val_type) result(name)
      75              :     integer, intent(in) :: val_type
      76              :     character(len=:), allocatable :: name
      77              : 
      78            6 :     select case (val_type)
      79              :     case (VALUE_TYPE_NONE)
      80            1 :       name = "none"
      81              :     case (VALUE_TYPE_STRING)
      82            2 :       name = "string"
      83              :     case (VALUE_TYPE_INTEGER)
      84            2 :       name = "integer"
      85              :     case (VALUE_TYPE_REAL)
      86            0 :       name = "real"
      87              :     case (VALUE_TYPE_LOGICAL)
      88            0 :       name = "logical"
      89              :     case (VALUE_TYPE_ARRAY)
      90            0 :       name = "array"
      91              :     case (VALUE_TYPE_COMPLEX)
      92            0 :       name = "complex"
      93              :     case default
      94            0 :       name = "unknown"
      95              :     end select
      96              : 
      97           13 :   end function type_name
      98              : 
      99              :   !> Validate that a real value is within a specified range
     100           17 :   subroutine hsd_validate_range(table, path, min_val, max_val, error, context)
     101              :     type(hsd_table), intent(in), target :: table
     102              :     character(len=*), intent(in) :: path
     103              :     real(dp), intent(in) :: min_val, max_val
     104              :     type(hsd_error_t), allocatable, intent(out) :: error
     105              :     character(len=*), intent(in), optional :: context
     106              : 
     107              :     class(hsd_node), pointer :: child
     108           17 :     real(dp) :: val
     109           17 :     integer :: local_stat
     110              :     character(len=32) :: min_str, max_str, val_str
     111           17 :     character(len=:), allocatable :: ctx_prefix
     112              : 
     113           17 :     call hsd_get_child(table, path, child, local_stat)
     114              : 
     115           17 :     if (present(context)) then
     116            2 :       ctx_prefix = context // ": "
     117              :     else
     118           15 :       ctx_prefix = ""
     119              :     end if
     120              : 
     121           17 :     if (local_stat /= HSD_STAT_OK .or. .not. associated(child)) then
     122              :       call make_error(error, local_stat, &
     123            2 :         ctx_prefix // "Field '" // path // "' not found or invalid type for range validation")
     124            6 :       return
     125              :     end if
     126              : 
     127              :     select type (child)
     128              :     type is (hsd_value)
     129           15 :       call child%get_real(val, local_stat)
     130           15 :       if (local_stat /= HSD_STAT_OK) then
     131              :         call make_error(error, HSD_STAT_TYPE_ERROR, &
     132            4 :           ctx_prefix // "Field '" // path // "' is not a real number")
     133            4 :         return
     134              :       end if
     135              : 
     136           26 :       if (val < min_val .or. val > max_val) then
     137            7 :         write(min_str, '(G0)') min_val
     138            7 :         write(max_str, '(G0)') max_val
     139            7 :         write(val_str, '(G0)') val
     140              :         call make_error(error, HSD_STAT_TYPE_ERROR, &
     141              :           ctx_prefix // "Field '" // path // "' value " // trim(val_str) // &
     142            7 :           " is outside valid range [" // trim(min_str) // ", " // trim(max_str) // "]")
     143              :       end if
     144              :     class default
     145              :       call make_error(error, HSD_STAT_TYPE_ERROR, &
     146            0 :         ctx_prefix // "Field '" // path // "' is not a value node")
     147              :     end select
     148              : 
     149           22 :   end subroutine hsd_validate_range
     150              : 
     151              :   !> Validate that a string value is one of the allowed choices
     152           16 :   subroutine hsd_validate_one_of(table, path, choices, error, context)
     153              :     type(hsd_table), intent(in), target :: table
     154              :     character(len=*), intent(in) :: path
     155              :     character(len=*), intent(in) :: choices(:)
     156              :     type(hsd_error_t), allocatable, intent(out) :: error
     157              :     character(len=*), intent(in), optional :: context
     158              : 
     159              :     class(hsd_node), pointer :: child
     160            8 :     character(len=:), allocatable :: val, choices_str, ctx_prefix
     161            8 :     integer :: i, local_stat
     162            8 :     logical :: found
     163              : 
     164            8 :     call hsd_get_child(table, path, child, local_stat)
     165              : 
     166            8 :     if (present(context)) then
     167            1 :       ctx_prefix = context // ": "
     168              :     else
     169            7 :       ctx_prefix = ""
     170              :     end if
     171              : 
     172            8 :     if (local_stat /= HSD_STAT_OK .or. .not. associated(child)) then
     173              :       call make_error(error, local_stat, &
     174            1 :         ctx_prefix // "Field '" // path // "' not found")
     175            1 :       return
     176              :     end if
     177              : 
     178              :     select type (child)
     179              :     type is (hsd_value)
     180            6 :       call child%get_string(val, local_stat)
     181            6 :       if (local_stat /= HSD_STAT_OK) then
     182              :         call make_error(error, HSD_STAT_TYPE_ERROR, &
     183            0 :           ctx_prefix // "Field '" // path // "' is not a string")
     184            0 :         return
     185              :       end if
     186              : 
     187            6 :       found = .false.
     188           18 :       do i = 1, size(choices)
     189           46 :         if (to_lower(val) == to_lower(choices(i))) then
     190            2 :           found = .true.
     191           28 :           exit
     192              :         end if
     193              :       end do
     194              : 
     195           12 :       if (.not. found) then
     196            4 :         choices_str = ""
     197           15 :         do i = 1, size(choices)
     198           18 :           if (i > 1) choices_str = choices_str // ", "
     199           15 :           choices_str = choices_str // "'" // trim(choices(i)) // "'"
     200              :         end do
     201              :         call make_error(error, HSD_STAT_TYPE_ERROR, &
     202              :           ctx_prefix // "Field '" // path // "' value '" // val // &
     203            4 :           "' is not one of: " // choices_str)
     204              :       end if
     205              :     class default
     206              :       call make_error(error, HSD_STAT_TYPE_ERROR, &
     207            1 :         ctx_prefix // "Field '" // path // "' is not a value node")
     208              :     end select
     209              : 
     210           33 :   end subroutine hsd_validate_one_of
     211              : 
     212              :   !> Get a real value with automatic unit conversion
     213              :   !>
     214              :   !> The unit is read from the node's attribute and converted to the target unit.
     215              :   !> The converter function takes (value, from_unit, to_unit) and returns the converted value.
     216              :   !>
     217              :   !> Example:
     218              :   !>   For input `Temperature [Kelvin] = 300`, calling
     219              :   !>   `hsd_get_with_unit(root, "Temperature", val, "Celsius", converter)`
     220              :   !>   would call `converter(300.0, "Kelvin", "Celsius")` to get the result.
     221            6 :   subroutine hsd_get_with_unit(table, path, val, target_unit, converter, stat)
     222              :     type(hsd_table), intent(in), target :: table
     223              :     character(len=*), intent(in) :: path
     224              :     real(dp), intent(out) :: val
     225              :     character(len=*), intent(in) :: target_unit
     226              :     interface
     227              :       pure function converter(value, from_unit, to_unit) result(converted)
     228              :         import :: dp
     229              :         implicit none (type, external)
     230              :         real(dp), intent(in) :: value
     231              :         character(len=*), intent(in) :: from_unit, to_unit
     232              :         real(dp) :: converted
     233              :       end function converter
     234              :     end interface
     235              :     integer, intent(out), optional :: stat
     236              : 
     237              :     class(hsd_node), pointer :: child
     238            6 :     character(len=:), allocatable :: source_unit
     239            6 :     real(dp) :: raw_val
     240            6 :     integer :: local_stat
     241              : 
     242            6 :     val = 0.0_dp
     243              : 
     244            6 :     call hsd_get_child(table, path, child, local_stat)
     245            6 :     if (local_stat /= 0 .or. .not. associated(child)) then
     246            1 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     247            2 :       return
     248              :     end if
     249              : 
     250              :     select type (child)
     251              :     type is (hsd_value)
     252            4 :       call child%get_real(raw_val, local_stat)
     253            4 :       if (local_stat /= HSD_STAT_OK) then
     254            1 :         if (present(stat)) stat = local_stat
     255            1 :         return
     256              :       end if
     257              : 
     258            3 :       if (allocated(child%attrib)) then
     259            1 :         source_unit = child%attrib
     260              :       else
     261            2 :         source_unit = target_unit  ! No conversion needed
     262              :       end if
     263              : 
     264            3 :       val = converter(raw_val, source_unit, target_unit)
     265            7 :       if (present(stat)) stat = HSD_STAT_OK
     266              : 
     267              :     class default
     268            1 :       if (present(stat)) stat = HSD_STAT_TYPE_ERROR
     269              :     end select
     270              : 
     271           20 :   end subroutine hsd_get_with_unit
     272              : 
     273           50 : end module hsd_validation
        

Generated by: LCOV version 2.0-1