!> @file config_schema.f90 !> @brief Runtime-queryable schema and typed accessors for `config_t`. !! !! This module exposes the solver-owned metadata needed by adapters: !! parameter names, groups, value kinds, bounds, and choice lists. It also !! provides typed getters/setters so higher-level integrations do not need to !! know the layout of `config_t`. !! !! Internals are derived from the shared `config_engine` field-descriptor !! table (built once per process by `config.build_field_table`) rather than a !! hand-maintained `select case` per field; the public API is unchanged. module config_schema use precision, only: wp use config_engine, only: cfg_kind_int, cfg_kind_real, cfg_kind_logical, & cfg_kind_choice, cfg_kind_string, cfg_kind_real3, & field_desc_t, field_index, & fields_get_integer, fields_get_real, fields_get_logical, & fields_get_string, fields_get_real3, & fields_set_integer, fields_set_real, fields_set_logical, & fields_set_string, fields_set_real3 use config, only: config_t, build_field_table, n_config_fields use string_utils, only: lowercase_token implicit none private public :: cfg_kind_int, cfg_kind_real, cfg_kind_logical, cfg_kind_choice, & cfg_kind_string, cfg_kind_real3 !> Metadata published for one runtime-queryable configuration key. !! !! This structure is the authoritative schema record surfaced through the !! Fortran session layer, the C ABI, and the Python binding. The table order !! is stable and intentionally exposed through 1-based schema indices. type, public :: config_schema_entry_t character(len=32) :: key = '' !< Canonical lowercase parameter name. character(len=32) :: group = '' !< Namelist group such as `grid` or `output`. integer :: value_kind = 0 !< One of the `cfg_kind_*` tags above. character(len=256) :: help = '' !< Short user-facing help text. logical :: has_min = .false. !< True when `min_value` is meaningful. real(wp) :: min_value = 0.0_wp !< Inclusive lower bound when present. logical :: has_max = .false. !< True when `max_value` is meaningful. real(wp) :: max_value = 0.0_wp !< Inclusive upper bound when present. integer :: choice_set = 0 !< Nonzero iff the field publishes a choice list. end type config_schema_entry_t !> Static schema state: one saved table bound to a saved default instance. type(config_t), save, target :: schema_defaults type(field_desc_t), save :: schema_fields(n_config_fields) logical, save :: schema_ready = .false. public :: config_schema_count, get_config_schema_entry, find_config_schema_entry public :: config_schema_choice_count, get_config_schema_choice public :: config_get_integer, config_get_real, config_get_logical public :: config_get_string, config_get_real3 public :: config_set_integer, config_set_real, config_set_logical public :: config_set_string, config_set_real3 public :: config_default_integer, config_default_real, config_default_logical public :: config_default_string, config_default_real3 contains !> Return the number of published schema entries. integer function config_schema_count() result(count) call ensure_schema() count = n_config_fields end function config_schema_count !> Find a schema entry by key and return its 1-based index. !! !! Returns `0` when the key is not part of the published schema. integer function find_config_schema_entry(key) result(index) character(len=*), intent(in) :: key call ensure_schema() index = field_index(schema_fields, key) end function find_config_schema_entry !> Copy one schema entry by its 1-based index. !! !! Invalid indices leave `entry` reset to defaults and return `found = .false.`. subroutine get_config_schema_entry(index, entry, found) integer, intent(in) :: index type(config_schema_entry_t), intent(out) :: entry logical, intent(out) :: found call ensure_schema() if (index < 1 .or. index > n_config_fields) then found = .false. entry = config_schema_entry_t() return end if found = .true. associate (f => schema_fields(index)) entry % key = f % key entry % group = f % group entry % value_kind = f % value_kind entry % help = f % help entry % has_min = f % has_min entry % min_value = f % min_value entry % has_max = f % has_max entry % max_value = f % max_value ! choice_set kept ABI-compatible in spirit: nonzero iff a choice list exists. entry % choice_set = 0 if (allocated(f % choices)) entry % choice_set = 1 end associate end subroutine get_config_schema_entry !> Return the number of allowed choices for a choice-valued schema entry. !! !! Non-choice entries, or out-of-range indices, report `0`. integer function config_schema_choice_count(index) result(count) integer, intent(in) :: index call ensure_schema() count = 0 if (index >= 1 .and. index <= n_config_fields) then if (allocated(schema_fields(index) % choices)) count = size(schema_fields(index) % choices) end if end function config_schema_choice_count !> Copy one allowed string token from a schema entry's choice list. !! !! `choice_index` is 1-based to match the ABI-facing schema table order. subroutine get_config_schema_choice(index, choice_index, value, found) integer, intent(in) :: index, choice_index character(len=*), intent(out) :: value logical, intent(out) :: found call ensure_schema() value = '' found = .false. if (index >= 1 .and. index <= n_config_fields) then if (allocated(schema_fields(index) % choices)) then call copy_choice(schema_fields(index) % choices, choice_index, value, found) end if end if end subroutine get_config_schema_choice !> Read the compiled-in default integer value for a schema key. subroutine config_default_integer(key, value, is_ok, message) character(len=*), intent(in) :: key integer, intent(out) :: value logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(config_t) :: defaults call config_get_integer(defaults, key, value, is_ok, message) end subroutine config_default_integer !> Read the compiled-in default scalar real value for a schema key. subroutine config_default_real(key, value, is_ok, message) character(len=*), intent(in) :: key real(wp), intent(out) :: value logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(config_t) :: defaults call config_get_real(defaults, key, value, is_ok, message) end subroutine config_default_real !> Read the compiled-in default logical value for a schema key. subroutine config_default_logical(key, value, is_ok, message) character(len=*), intent(in) :: key logical, intent(out) :: value logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(config_t) :: defaults call config_get_logical(defaults, key, value, is_ok, message) end subroutine config_default_logical !> Read the compiled-in default string or choice token for a schema key. subroutine config_default_string(key, value, is_ok, message) character(len=*), intent(in) :: key character(len=*), intent(out) :: value logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(config_t) :: defaults call config_get_string(defaults, key, value, is_ok, message) end subroutine config_default_string !> Read the compiled-in default length-3 real vector for a schema key. subroutine config_default_real3(key, value, is_ok, message) character(len=*), intent(in) :: key real(wp), intent(out) :: value(3) logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(config_t) :: defaults call config_get_real3(defaults, key, value, is_ok, message) end subroutine config_default_real3 !> Read one integer-valued field from `cfg` by schema key. subroutine config_get_integer(cfg, key, value, is_ok, message) type(config_t), intent(in) :: cfg character(len=*), intent(in) :: key integer, intent(out) :: value logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(config_t), target :: work type(field_desc_t) :: fields(n_config_fields) character(len=32) :: normalized call set_status(.true., '', is_ok, message) work = cfg fields = build_field_table(work) if (.not. fields_get_integer(fields, key, value)) then normalized = normalize_key(key) call set_status(.false., 'config_schema: "'//trim(normalized)// & '" is not an integer parameter', is_ok, message) end if end subroutine config_get_integer !> Read one scalar real-valued field from `cfg` by schema key. subroutine config_get_real(cfg, key, value, is_ok, message) type(config_t), intent(in) :: cfg character(len=*), intent(in) :: key real(wp), intent(out) :: value logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(config_t), target :: work type(field_desc_t) :: fields(n_config_fields) character(len=32) :: normalized call set_status(.true., '', is_ok, message) work = cfg fields = build_field_table(work) if (.not. fields_get_real(fields, key, value)) then normalized = normalize_key(key) call set_status(.false., 'config_schema: "'//trim(normalized)// & '" is not a real scalar parameter', is_ok, message) end if end subroutine config_get_real !> Read one logical field from `cfg` by schema key. subroutine config_get_logical(cfg, key, value, is_ok, message) type(config_t), intent(in) :: cfg character(len=*), intent(in) :: key logical, intent(out) :: value logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(config_t), target :: work type(field_desc_t) :: fields(n_config_fields) character(len=32) :: normalized call set_status(.true., '', is_ok, message) work = cfg fields = build_field_table(work) if (.not. fields_get_logical(fields, key, value)) then normalized = normalize_key(key) call set_status(.false., 'config_schema: "'//trim(normalized)// & '" is not a logical parameter', is_ok, message) end if end subroutine config_get_logical !> Read one string or choice token from `cfg` by schema key. subroutine config_get_string(cfg, key, value, is_ok, message) type(config_t), intent(in) :: cfg character(len=*), intent(in) :: key character(len=*), intent(out) :: value logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(config_t), target :: work type(field_desc_t) :: fields(n_config_fields) character(len=32) :: normalized call set_status(.true., '', is_ok, message) work = cfg fields = build_field_table(work) if (.not. fields_get_string(fields, key, value)) then normalized = normalize_key(key) call set_status(.false., 'config_schema: "'//trim(normalized)// & '" is not a string/choice parameter', is_ok, message) end if end subroutine config_get_string !> Read one fixed-length real-3 field from `cfg` by schema key. subroutine config_get_real3(cfg, key, value, is_ok, message) type(config_t), intent(in) :: cfg character(len=*), intent(in) :: key real(wp), intent(out) :: value(3) logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(config_t), target :: work type(field_desc_t) :: fields(n_config_fields) character(len=32) :: normalized call set_status(.true., '', is_ok, message) work = cfg fields = build_field_table(work) if (.not. fields_get_real3(fields, key, value)) then normalized = normalize_key(key) call set_status(.false., 'config_schema: "'//trim(normalized)// & '" is not a 3-vector parameter', is_ok, message) end if end subroutine config_get_real3 !> Write one integer-valued field in `cfg` by schema key. !! !! This is a typed field assignment helper; cross-field validation still !! happens separately through `validate_config`. subroutine config_set_integer(cfg, key, value, is_ok, message) type(config_t), intent(inout), target :: cfg character(len=*), intent(in) :: key integer, intent(in) :: value logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(field_desc_t) :: fields(n_config_fields) character(len=32) :: normalized call set_status(.true., '', is_ok, message) fields = build_field_table(cfg) if (.not. fields_set_integer(fields, key, value)) then normalized = normalize_key(key) call set_status(.false., 'config_schema: "'//trim(normalized)// & '" is not an integer parameter', is_ok, message) end if end subroutine config_set_integer !> Write one scalar real-valued field in `cfg` by schema key. subroutine config_set_real(cfg, key, value, is_ok, message) type(config_t), intent(inout), target :: cfg character(len=*), intent(in) :: key real(wp), intent(in) :: value logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(field_desc_t) :: fields(n_config_fields) character(len=32) :: normalized call set_status(.true., '', is_ok, message) fields = build_field_table(cfg) if (.not. fields_set_real(fields, key, value)) then normalized = normalize_key(key) call set_status(.false., 'config_schema: "'//trim(normalized)// & '" is not a real scalar parameter', is_ok, message) end if end subroutine config_set_real !> Write one logical field in `cfg` by schema key. subroutine config_set_logical(cfg, key, value, is_ok, message) type(config_t), intent(inout), target :: cfg character(len=*), intent(in) :: key logical, intent(in) :: value logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(field_desc_t) :: fields(n_config_fields) character(len=32) :: normalized call set_status(.true., '', is_ok, message) fields = build_field_table(cfg) if (.not. fields_set_logical(fields, key, value)) then normalized = normalize_key(key) call set_status(.false., 'config_schema: "'//trim(normalized)// & '" is not a logical parameter', is_ok, message) end if end subroutine config_set_logical !> Write one string or choice token in `cfg` by schema key. subroutine config_set_string(cfg, key, value, is_ok, message) type(config_t), intent(inout), target :: cfg character(len=*), intent(in) :: key character(len=*), intent(in) :: value logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(field_desc_t) :: fields(n_config_fields) character(len=32) :: normalized call set_status(.true., '', is_ok, message) fields = build_field_table(cfg) if (.not. fields_set_string(fields, key, value)) then normalized = normalize_key(key) call set_status(.false., 'config_schema: "'//trim(normalized)// & '" is not a string/choice parameter', is_ok, message) end if end subroutine config_set_string !> Write one fixed-length real-3 field in `cfg` by schema key. subroutine config_set_real3(cfg, key, value, is_ok, message) type(config_t), intent(inout), target :: cfg character(len=*), intent(in) :: key real(wp), intent(in) :: value(3) logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message type(field_desc_t) :: fields(n_config_fields) character(len=32) :: normalized call set_status(.true., '', is_ok, message) fields = build_field_table(cfg) if (.not. fields_set_real3(fields, key, value)) then normalized = normalize_key(key) call set_status(.false., 'config_schema: "'//trim(normalized)// & '" is not a 3-vector parameter', is_ok, message) end if end subroutine config_set_real3 !> Populate the saved schema table on first use, bound to a saved default !! `config_t` instance (the single-source-of-defaults property the spec !! requires). subroutine ensure_schema() if (schema_ready) return schema_fields = build_field_table(schema_defaults) schema_ready = .true. end subroutine ensure_schema !> Normalise a user-provided parameter key to lowercase trimmed form. !! Thin wrapper over `lowercase_token` (from `string_utils`) fixing the !! result to the schema's 32-character key field width. pure function normalize_key(key) result(normalized) character(len=*), intent(in) :: key character(len=32) :: normalized normalized = lowercase_token(key) end function normalize_key !> Copy one 1-based choice-list item into `value`. subroutine copy_choice(values, index, value, found) character(len=*), intent(in) :: values(:) integer, intent(in) :: index character(len=*), intent(out) :: value logical, intent(out) :: found value = '' if (index < 1 .or. index > size(values)) then found = .false. return end if found = .true. value = trim(values(index)) end subroutine copy_choice !> Fan out a success flag and optional message to caller-provided outputs. subroutine set_status(ok, err, is_ok, message) logical, intent(in) :: ok character(len=*), intent(in) :: err logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message if (present(is_ok)) is_ok = ok if (present(message)) message = trim(err) end subroutine set_status end module config_schema