44"""This module defines the CharmState class which represents the state of the charm."""
55import logging
66import os
7+ import pathlib
78import re
89import typing
910from dataclasses import dataclass , field
1011from typing import Optional , Type , TypeVar
1112
1213from charms .data_platform_libs .v0 .data_interfaces import DatabaseRequires
1314from charms .redis_k8s .v0 .redis import RedisRequires
14- from pydantic import BaseModel , Extra , Field , ValidationError , ValidationInfo , field_validator
15+ from pydantic import (
16+ BaseModel ,
17+ Extra ,
18+ Field ,
19+ ValidationError ,
20+ ValidationInfo ,
21+ create_model ,
22+ field_validator ,
23+ )
1524
1625from paas_charm .databases import get_uri
1726from paas_charm .exceptions import CharmConfigInvalidError
1827from paas_charm .rabbitmq import RabbitMQRequires
1928from paas_charm .secret_storage import KeySecretStorage
20- from paas_charm .utils import build_validation_error_message
29+ from paas_charm .utils import build_validation_error_message , config_metadata
2130
2231logger = logging .getLogger (__name__ )
2332
@@ -55,7 +64,7 @@ class CharmState: # pylint: disable=too-many-instance-attributes
5564
5665 Attrs:
5766 framework_config: the value of the framework specific charm configuration.
58- app_config : user-defined configurations for the application.
67+ user_defined_config : user-defined configurations for the application.
5968 secret_key: the charm managed application secret key.
6069 is_secret_storage_ready: whether the secret storage system is ready.
6170 proxy: proxy information.
@@ -66,7 +75,7 @@ def __init__( # pylint: disable=too-many-arguments
6675 * ,
6776 framework : str ,
6877 is_secret_storage_ready : bool ,
69- app_config : dict [str , int | str | bool | dict [str , str ]] | None = None ,
78+ user_defined_config : dict [str , int | str | bool | dict [str , str ]] | None = None ,
7079 framework_config : dict [str , int | str ] | None = None ,
7180 secret_key : str | None = None ,
7281 integrations : "IntegrationsState | None" = None ,
@@ -77,15 +86,15 @@ def __init__( # pylint: disable=too-many-arguments
7786 Args:
7887 framework: the framework name.
7988 is_secret_storage_ready: whether the secret storage system is ready.
80- app_config : User-defined configuration values for the application configuration .
89+ user_defined_config : User-defined configuration values for the application.
8190 framework_config: The value of the framework application specific charm configuration.
8291 secret_key: The secret storage manager associated with the charm.
8392 integrations: Information about the integrations.
8493 base_url: Base URL for the service.
8594 """
8695 self .framework = framework
8796 self ._framework_config = framework_config if framework_config is not None else {}
88- self ._app_config = app_config if app_config is not None else {}
97+ self ._user_defined_config = user_defined_config if user_defined_config is not None else {}
8998 self ._is_secret_storage_ready = is_secret_storage_ready
9099 self ._secret_key = secret_key
91100 self .integrations = integrations or IntegrationsState ()
@@ -116,16 +125,27 @@ def from_charm( # pylint: disable=too-many-arguments
116125
117126 Return:
118127 The CharmState instance created by the provided charm.
128+
129+ Raises:
130+ CharmConfigInvalidError: If some parameter in invalid.
119131 """
120- app_config = {
132+ user_defined_config = {
121133 k .replace ("-" , "_" ): v
122134 for k , v in config .items ()
123- if not any ( k . startswith ( prefix ) for prefix in ( f" { framework } -" , "webserver-" , "app-" ) )
135+ if is_user_defined_config ( k , framework )
124136 }
125- app_config = {
126- k : v for k , v in app_config .items () if k not in framework_config .dict ().keys ()
137+ user_defined_config = {
138+ k : v for k , v in user_defined_config .items () if k not in framework_config .dict ().keys ()
127139 }
128140
141+ app_config_class = app_config_class_factory (framework )
142+ try :
143+ app_config_class (** user_defined_config )
144+ except ValidationError as exc :
145+ error_messages = build_validation_error_message (exc , underscore_to_dash = True )
146+ logger .error (error_messages .long )
147+ raise CharmConfigInvalidError (error_messages .short ) from exc
148+
129149 saml_relation_data = None
130150 if integration_requirers .saml and (
131151 saml_data := integration_requirers .saml .get_relation_data ()
@@ -153,7 +173,9 @@ def from_charm( # pylint: disable=too-many-arguments
153173 return cls (
154174 framework = framework ,
155175 framework_config = framework_config .dict (exclude_none = True ),
156- app_config = typing .cast (dict [str , str | int | bool | dict [str , str ]], app_config ),
176+ user_defined_config = typing .cast (
177+ dict [str , str | int | bool | dict [str , str ]], user_defined_config
178+ ),
157179 secret_key = (
158180 secret_storage .get_secret_key () if secret_storage .is_initialized else None
159181 ),
@@ -188,13 +210,13 @@ def framework_config(self) -> dict[str, str | int | bool]:
188210 return self ._framework_config
189211
190212 @property
191- def app_config (self ) -> dict [str , str | int | bool | dict [str , str ]]:
213+ def user_defined_config (self ) -> dict [str , str | int | bool | dict [str , str ]]:
192214 """Get the value of user-defined application configurations.
193215
194216 Returns:
195217 The value of user-defined application configurations.
196218 """
197- return self ._app_config
219+ return self ._user_defined_config
198220
199221 @property
200222 def secret_key (self ) -> str :
@@ -351,9 +373,10 @@ def generate_relation_parameters(
351373 try :
352374 return parameter_type .parse_obj (relation_data )
353375 except ValidationError as exc :
354- error_message = build_validation_error_message (exc )
376+ error_messages = build_validation_error_message (exc )
377+ logger .error (error_messages .long )
355378 raise CharmConfigInvalidError (
356- f"Invalid { parameter_type .__name__ } configuration : { error_message } "
379+ f"Invalid { parameter_type .__name__ } : { error_messages . short } "
357380 ) from exc
358381
359382
@@ -458,3 +481,76 @@ def validate_signing_certificate_exists(cls, certs: str, _: ValidationInfo) -> s
458481 if not certificate :
459482 raise ValueError ("Missing x509certs. There should be at least one certificate." )
460483 return certificate
484+
485+
486+ def _create_config_attribute (option_name : str , option : dict ) -> tuple [str , tuple ]:
487+ """Create the configuration attribute.
488+
489+ Args:
490+ option_name: Name of the configuration option.
491+ option: The configuration option data.
492+
493+ Raises:
494+ ValueError: raised when the option type is not valid.
495+
496+ Returns:
497+ A tuple constructed from attribute name and type.
498+ """
499+ option_name = option_name .replace ("-" , "_" )
500+ optional = option .get ("optional" ) is not False
501+ config_type_str = option .get ("type" )
502+
503+ config_type : type [bool ] | type [int ] | type [float ] | type [str ] | type [dict ]
504+ match config_type_str :
505+ case "boolean" :
506+ config_type = bool
507+ case "int" :
508+ config_type = int
509+ case "float" :
510+ config_type = float
511+ case "string" :
512+ config_type = str
513+ case "secret" :
514+ config_type = dict
515+ case _:
516+ raise ValueError (f"Invalid option type: { config_type_str } ." )
517+
518+ type_tuple : tuple = (config_type , Field ())
519+ if optional :
520+ type_tuple = (config_type | None , None )
521+
522+ return (option_name , type_tuple )
523+
524+
525+ def app_config_class_factory (framework : str ) -> type [BaseModel ]:
526+ """App config class factory.
527+
528+ Args:
529+ framework: The framework name.
530+
531+ Returns:
532+ Constructed app config class.
533+ """
534+ config_options = config_metadata (pathlib .Path (os .getcwd ()))["options" ]
535+ model_attributes = dict (
536+ _create_config_attribute (option_name , config_options [option_name ])
537+ for option_name in config_options
538+ if is_user_defined_config (option_name , framework )
539+ )
540+ # mypy doesn't like the model_attributes dict
541+ return create_model ("AppConfig" , ** model_attributes ) # type: ignore[call-overload]
542+
543+
544+ def is_user_defined_config (option_name : str , framework : str ) -> bool :
545+ """Check if a config option is user defined.
546+
547+ Args:
548+ option_name: Name of the config option.
549+ framework: The framework name.
550+
551+ Returns:
552+ True if user defined config options, false otherwise.
553+ """
554+ return not any (
555+ option_name .startswith (prefix ) for prefix in (f"{ framework } -" , "webserver-" , "app-" )
556+ )
0 commit comments