Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion flexmeasures/data/schemas/scheduling/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,16 @@ def check_prices(self, data: dict, original_data: dict, **kwargs):
},
}

UI_FLEX_MODEL_SCHEMA: Dict[str, Dict[str, Any]] = {
# Per-field UI presentation info for the storage flex-model editor.
#
# This registry does NOT define which flex-model fields exist: the field/key set
# is derived from ``DBStorageFlexModelSchema`` (the single source of truth) by
# ``_build_ui_flex_model_schema`` below. This registry only supplies the UI
# presentation values (default, description, editor type token + help string, and
# example units) for each field. Adding a field to the DB schema without a matching
# entry here therefore fails loudly at import time (see the builder), instead of
# silently breaking DB<->UI parity and only surfacing in a full CI run.
_UI_FLEX_MODEL_PRESENTATION: Dict[str, Dict[str, Any]] = {
"consumption": {
"default": None,
"description": rst_to_openapi(metadata.CONSUMPTION.description),
Expand Down Expand Up @@ -1226,6 +1235,43 @@ def check_prices(self, data: dict, original_data: dict, **kwargs):
}


def _build_ui_flex_model_schema() -> Dict[str, Dict[str, Any]]:
"""Build the UI flex-model schema from the DB storage flex-model schema.

``DBStorageFlexModelSchema`` is the single source of truth for which flex-model
fields exist. For each of its fields we look up the UI presentation info in
``_UI_FLEX_MODEL_PRESENTATION``. If a DB field has no presentation entry, we
raise right here (at import time), so a newly added flex-model field can no
longer silently break DB<->UI parity (it fails immediately, with a clear
message pointing at the fix, rather than only in a full CI run).
"""
# Imported lazily to avoid an import cycle at module load time.
from flexmeasures.data.schemas.scheduling.storage import DBStorageFlexModelSchema

schema: Dict[str, Dict[str, Any]] = {}
missing: list[str] = []
for field_name, field in DBStorageFlexModelSchema().fields.items():
key = field.data_key or field_name
entry = _UI_FLEX_MODEL_PRESENTATION.get(key)
if entry is None:
missing.append(key)
continue
schema[key] = entry
if missing:
raise ValueError(
"Missing UI presentation info for DBStorageFlexModelSchema field(s): "
f"{missing}. Add an entry for each in `_UI_FLEX_MODEL_PRESENTATION` "
"(flexmeasures/data/schemas/scheduling/__init__.py) so the UI flex-model "
"editor stays in sync with the DB flex-model schema."
)
return schema


# Derived from the DB schema, so the DB schema is the single source of truth for
# which flex-model fields the UI editor exposes (see _build_ui_flex_model_schema).
UI_FLEX_MODEL_SCHEMA: Dict[str, Dict[str, Any]] = _build_ui_flex_model_schema()


class DBFlexContextSchema(FlexContextSchema, NoTimeSeriesSpecs):

commitments = fields.Nested(
Expand Down
49 changes: 34 additions & 15 deletions flexmeasures/ui/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,22 +101,41 @@ def test_ui_flexcontext_schema():

def test_ui_flexmodel_schema():
"""
This test ensures that all fields in the DBStorageFlexModelSchema are also in the UI schema and vice versa.

This is important to keep in mind when updating either schema. We want to avoid a situation
where a field is added to the DB schema but not to the UI schema, as that would lead to
inconsistencies and potential bugs in the application.
UI_FLEX_MODEL_SCHEMA is now derived from DBStorageFlexModelSchema (the single
source of truth) by _build_ui_flex_model_schema, so DB<->UI parity is guaranteed
by construction: a DB field without UI presentation info raises at import time.

Here we assert the remaining property that cannot be guaranteed structurally:
that every derived UI entry is well-formed (has the expected keys and a
non-empty backend type token). If someone adds a flex-model field but leaves
its UI presentation info incomplete, this fails with a clear message.
"""
ui_flexmodel_schema_fields = [key for key, value in UI_FLEX_MODEL_SCHEMA.items()]

schema_keys = []
for value in DBStorageFlexModelSchema().fields.values():
schema_keys.append(value.data_key if value.data_key else value.name)

schema_keys = set(schema_keys)
ui_flexmodel_schema_fields = set(ui_flexmodel_schema_fields)

assert schema_keys == ui_flexmodel_schema_fields
schema_keys = {
(value.data_key if value.data_key else value.name)
for value in DBStorageFlexModelSchema().fields.values()
}

# Parity is by construction: the derived schema covers exactly the DB fields.
assert set(UI_FLEX_MODEL_SCHEMA.keys()) == schema_keys

for key, entry in UI_FLEX_MODEL_SCHEMA.items():
assert set(entry.keys()) == {
"default",
"description",
"types",
"example-units",
}, f"UI flex-model entry '{key}' has unexpected keys: {sorted(entry.keys())}"
assert set(entry["types"].keys()) == {
"backend",
"ui",
}, f"UI flex-model entry '{key}' has a malformed 'types' sub-dict."
assert entry["types"][
"backend"
], f"UI flex-model entry '{key}' has an empty backend type token."
assert entry["types"][
"ui"
], f"UI flex-model entry '{key}' has an empty UI help string."
assert entry["description"], f"UI flex-model entry '{key}' has no description."


class NewAsset:
Expand Down
Loading