From 9cbff48ae334f292250753779ca2a4a3b307455b Mon Sep 17 00:00:00 2001 From: Adrian Zawadzki Date: Tue, 19 May 2026 13:10:40 +0200 Subject: [PATCH 1/3] feature(field_protect): added inheritance for getters and setters --- python_tests/test_memo_protect_fields.py | 143 ++++++++++++++++++++++- src/dbzero/object_model/class/Class.cpp | 54 ++++++--- 2 files changed, 177 insertions(+), 20 deletions(-) diff --git a/python_tests/test_memo_protect_fields.py b/python_tests/test_memo_protect_fields.py index c0bef517..16205629 100644 --- a/python_tests/test_memo_protect_fields.py +++ b/python_tests/test_memo_protect_fields.py @@ -577,7 +577,7 @@ def test_protected_field_getter_requires_read_access(db0_fixture): _ = obj.value -def test_protected_field_getter_does_not_inherit_base_class_masks(db0_fixture): +def test_protected_field_getter_inherits_base_class_masks(db0_fixture): account_id = ContextVar("protected_inheritance_account_id") obj = MemoImplicitlyProtectedDerivedFieldsClass("base", "alpha", 1, 1.5) db0.set_field_access(MemoProtectedDerivedFieldsClass, 123, (FieldAccess.READ,), "base_value", "name") @@ -585,15 +585,64 @@ def test_protected_field_getter_does_not_inherit_base_class_masks(db0_fixture): db0._init_data_masking(account_id) account_id.set(123) + assert obj.base_value == "base" + assert obj.name == "alpha" assert obj.derived_value == 1.5 - with pytest.raises(PermissionError, match="read"): - _ = obj.base_value - with pytest.raises(PermissionError, match="read"): - _ = obj.name with pytest.raises(PermissionError, match="read"): _ = obj.value +def test_protected_field_create_inherits_base_class_masks(db0_fixture): + account_id = ContextVar("protected_inheritance_create_account_id") + obj = MemoImplicitlyProtectedDerivedFieldsClass("base", "alpha", 1, 1.5) + db0.set_field_access(MemoProtectedDerivedFieldsClass, 101, (FieldAccess.READ,), "extra") + db0.set_field_access(MemoProtectedDerivedFieldsClass, 202, (FieldAccess.CREATE, FieldAccess.READ), "extra") + db0._init_data_masking(account_id) + + account_id.set(101) + with pytest.raises(PermissionError, match="create"): + obj.extra = "denied" + + account_id.set(202) + obj.extra = "allowed" + assert obj.extra == "allowed" + + +def test_protected_field_update_inherits_base_class_masks(db0_fixture): + account_id = ContextVar("protected_inheritance_update_account_id") + obj = MemoImplicitlyProtectedDerivedFieldsClass("base", "alpha", 1, 1.5) + db0.set_field_access(MemoProtectedDerivedFieldsClass, 101, (FieldAccess.READ,), "name") + db0.set_field_access(MemoProtectedDerivedFieldsClass, 202, (FieldAccess.READ, FieldAccess.UPDATE), "name") + db0._init_data_masking(account_id) + + account_id.set(101) + with pytest.raises(PermissionError, match="update"): + obj.name = "denied" + assert obj.name == "alpha" + + account_id.set(202) + obj.name = "allowed" + assert obj.name == "allowed" + + +def test_protected_field_delete_inherits_base_class_masks(db0_fixture): + account_id = ContextVar("protected_inheritance_delete_account_id") + obj = MemoImplicitlyProtectedDerivedFieldsClass("base", "alpha", 1, 1.5) + db0.set_field_access(MemoProtectedDerivedFieldsClass, 101, (FieldAccess.READ,), "name") + db0.set_field_access(MemoProtectedDerivedFieldsClass, 202, (FieldAccess.DELETE,), "name") + db0._init_data_masking(account_id) + + account_id.set(101) + with pytest.raises(PermissionError, match="delete"): + del obj.name + assert obj.name == "alpha" + + account_id.set(202) + del obj.name + with pytest.raises(AttributeError): + _ = obj.name + + def test_protected_field_getter_uses_derived_class_own_masks(db0_fixture): account_id = ContextVar("protected_inheritance_allow_override_account_id") obj = MemoImplicitlyProtectedDerivedFieldsClass("base", "alpha", 1, 1.5) @@ -686,7 +735,53 @@ class DerivedAfter(BaseAfter): obj = db0.fetch(DerivedAfter, obj_id) assert get_memo_class_object(obj).get_type_flags()["protect_fields"] is True + with pytest.raises(RuntimeError, match="data masking"): + _ = obj.value db0.set_field_access(DerivedAfter, 123, (FieldAccess.READ,), "value") + account_id = ContextVar("protected_base_enabled_later_account_id") + db0._init_data_masking(account_id) + account_id.set(123) + assert obj.value == 1 + + +def test_existing_derived_instance_inherits_base_field_access_enabled_after_materialization(db0_fixture): + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-access-enabled-later-base") + @dataclass + class BaseBefore: + base_value: str + + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-access-enabled-later-derived") + @dataclass + class DerivedBefore(BaseBefore): + value: int + + obj = DerivedBefore("base", 1) + obj_id = db0.uuid(obj) + db0.commit() + + db0.close() + db0.init(DB0_DIR) + db0.open("my-test-prefix") + + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-access-enabled-later-base", protect_fields=True) + @dataclass + class BaseAfter: + base_value: str + + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-access-enabled-later-derived") + @dataclass + class DerivedAfter(BaseAfter): + value: int + + obj = db0.fetch(DerivedAfter, obj_id) + db0.set_field_access(BaseAfter, 123, (FieldAccess.READ,), "base_value") + account_id = ContextVar("protected_base_access_enabled_later_account_id") + db0._init_data_masking(account_id) + account_id.set(123) + + assert obj.base_value == "base" + with pytest.raises(PermissionError, match="read"): + _ = obj.value def test_derived_class_stops_inheriting_protect_fields_after_base_reset(db0_fixture): @@ -722,11 +817,49 @@ class DerivedAfter(BaseAfter): obj = db0.fetch(DerivedAfter, obj_id) db0.reset_protect_fields(BaseAfter) + assert obj.value == 1 assert get_memo_class_object(obj).get_type_flags()["protect_fields"] is False with pytest.raises(RuntimeError, match="protected fields"): db0.set_field_access(DerivedAfter, 123, (FieldAccess.READ,), "value") +def test_existing_derived_instance_stops_using_base_field_access_after_base_reset(db0_fixture): + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-access-reset-base", protect_fields=True) + @dataclass + class BaseBefore: + base_value: str + + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-access-reset-derived") + @dataclass + class DerivedBefore(BaseBefore): + value: int + + obj = DerivedBefore("base", 1) + obj_id = db0.uuid(obj) + db0.commit() + + db0.close() + db0.init(DB0_DIR) + db0.open("my-test-prefix") + + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-access-reset-base", protect_fields=False) + @dataclass + class BaseAfter: + base_value: str + + @db0.memo(id="dbzero-software/dbzero/tests/protected-base-access-reset-derived") + @dataclass + class DerivedAfter(BaseAfter): + value: int + + obj = db0.fetch(DerivedAfter, obj_id) + db0.reset_protect_fields(BaseAfter) + + assert get_memo_class_object(obj).get_type_flags()["protect_fields"] is False + assert obj.base_value == "base" + assert obj.value == 1 + + def test_protected_field_getter_returns_missing_value_placeholder(db0_fixture): account_id = ContextVar("protected_placeholder_account_id") missing_value = object() diff --git a/src/dbzero/object_model/class/Class.cpp b/src/dbzero/object_model/class/Class.cpp index dbb83a32..73855177 100644 --- a/src/dbzero/object_model/class/Class.cpp +++ b/src/dbzero/object_model/class/Class.cpp @@ -439,15 +439,20 @@ namespace db0::object_model return {}; } - if (!hasFieldSafe()) { - return {}; + if (hasFieldSafe()) { + auto &field_safe = getFieldSafe(); + auto field_mask = field_safe.getFieldMaskManager().tryGetFieldMask(account_id); + auto maybe_offset = field_safe.getFieldIDMapper().tryGetAssignedFieldOffset(member.m_field_id); + if (maybe_offset && field_mask) { + auto mask = field_mask->getAssignedMask(*maybe_offset); + if (mask && !mask->none()) { + return mask; + } + } } - auto &field_safe = getFieldSafe(); - auto field_mask = field_safe.getFieldMaskManager().tryGetFieldMask(account_id); - auto maybe_offset = field_safe.getFieldIDMapper().tryGetAssignedFieldOffset(member.m_field_id); - if (maybe_offset && field_mask) { - return field_mask->getAssignedMask(*maybe_offset); + if (m_base_class_ptr && m_base_class_ptr->isProtectFields()) { + return m_base_class_ptr->tryGetFieldAccess(account_id, member.m_name.c_str()); } return {}; @@ -473,18 +478,37 @@ namespace db0::object_model return {}; } - auto &field_safe = getFieldSafe(); - auto maybe_offset = field_safe.getFieldIDMapper().tryGetAssignedFieldOffset(field_name); - if (!maybe_offset) { - return {}; + if (hasFieldSafe()) { + auto &field_safe = getFieldSafe(); + auto field_mask = field_safe.getFieldMaskManager().tryGetFieldMask(account_id); + if (field_mask) { + auto member = tryGetMember(field_name); + if (member) { + auto maybe_offset = field_safe.getFieldIDMapper().tryGetAssignedFieldOffset(member->m_field_id); + if (maybe_offset) { + auto mask = field_mask->getAssignedMask(*maybe_offset); + if (mask && !mask->none()) { + return mask; + } + } + } + + auto maybe_offset = field_safe.getFieldIDMapper().tryGetAssignedFieldOffset(field_name); + if (maybe_offset) { + auto mask = field_mask->getAssignedMask(*maybe_offset); + if (mask && !mask->none()) { + return mask; + } + } + } + } - auto field_mask = field_safe.getFieldMaskManager().tryGetFieldMask(account_id); - if (!field_mask) { - return {}; + if (m_base_class_ptr && m_base_class_ptr->isProtectFields()) { + return m_base_class_ptr->tryGetFieldAccess(account_id, field_name); } - return field_mask->getAssignedMask(*maybe_offset); + return {}; } std::vector > Class::getFieldAccess(std::uint64_t account_id) const From 6db4168d07ca39535ed58da7e68c425b8f311df4 Mon Sep 17 00:00:00 2001 From: Adrian Zawadzki Date: Tue, 19 May 2026 14:16:29 +0200 Subject: [PATCH 2/3] fix(pr): pull request fixes --- src/dbzero/bindings/python/Memo.cpp | 4 ++-- src/dbzero/object_model/class/Class.cpp | 29 ++++++++++++------------- src/dbzero/object_model/class/Class.hpp | 5 +++-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/dbzero/bindings/python/Memo.cpp b/src/dbzero/bindings/python/Memo.cpp index 9f98b9af..93501c62 100644 --- a/src/dbzero/bindings/python/Memo.cpp +++ b/src/dbzero/bindings/python/Memo.cpp @@ -434,9 +434,9 @@ namespace db0::python return false; } - auto mask = type->tryGetFieldAccess(static_cast(account_id), member_loc); + auto mask = type->tryGetFieldAccessByMemberLoc(static_cast(account_id), member_loc); if (!mask && field_name) { - mask = type->tryGetFieldAccess(static_cast(account_id), field_name); + mask = type->tryGetFieldAccessByName(static_cast(account_id), field_name); } return mask && (*mask)[access_option]; } diff --git a/src/dbzero/object_model/class/Class.cpp b/src/dbzero/object_model/class/Class.cpp index 73855177..6e2cd832 100644 --- a/src/dbzero/object_model/class/Class.cpp +++ b/src/dbzero/object_model/class/Class.cpp @@ -433,7 +433,7 @@ namespace db0::object_model } } - std::optional Class::tryGetFieldAccess(std::uint64_t account_id, const Member &member) const + std::optional Class::tryGetFieldAccessByMember(std::uint64_t account_id, const Member &member) const { if (!isProtectFields()) { return {}; @@ -458,7 +458,7 @@ namespace db0::object_model return {}; } - std::optional Class::tryGetFieldAccess(std::uint64_t account_id, const MemberLoc &member_loc) const + std::optional Class::tryGetFieldAccessByMemberLoc(std::uint64_t account_id, const MemberLoc &member_loc) const { const auto &member_id = member_loc.first; if (!member_id) { @@ -467,12 +467,22 @@ namespace db0::object_model auto member = tryGetMember(member_id.primary().first); if (member) { - return tryGetFieldAccess(account_id, *member); + return tryGetFieldAccessByMember(account_id, *member); } return {}; } std::optional Class::tryGetFieldAccess(std::uint64_t account_id, const char *field_name) const + { + auto member = tryGetMember(field_name); + if (member) { + return tryGetFieldAccessByMember(account_id, *member); + } + + return tryGetFieldAccessByName(account_id, field_name); + } + + std::optional Class::tryGetFieldAccessByName(std::uint64_t account_id, const char *field_name) const { if (!isProtectFields()) { return {}; @@ -482,17 +492,6 @@ namespace db0::object_model auto &field_safe = getFieldSafe(); auto field_mask = field_safe.getFieldMaskManager().tryGetFieldMask(account_id); if (field_mask) { - auto member = tryGetMember(field_name); - if (member) { - auto maybe_offset = field_safe.getFieldIDMapper().tryGetAssignedFieldOffset(member->m_field_id); - if (maybe_offset) { - auto mask = field_mask->getAssignedMask(*maybe_offset); - if (mask && !mask->none()) { - return mask; - } - } - } - auto maybe_offset = field_safe.getFieldIDMapper().tryGetAssignedFieldOffset(field_name); if (maybe_offset) { auto mask = field_mask->getAssignedMask(*maybe_offset); @@ -505,7 +504,7 @@ namespace db0::object_model } if (m_base_class_ptr && m_base_class_ptr->isProtectFields()) { - return m_base_class_ptr->tryGetFieldAccess(account_id, field_name); + return m_base_class_ptr->tryGetFieldAccessByName(account_id, field_name); } return {}; diff --git a/src/dbzero/object_model/class/Class.hpp b/src/dbzero/object_model/class/Class.hpp index 27bc5212..696620db 100644 --- a/src/dbzero/object_model/class/Class.hpp +++ b/src/dbzero/object_model/class/Class.hpp @@ -184,9 +184,10 @@ DB0_PACKED_END const FieldSafe &getFieldSafe() const; void setFieldAccess(const std::vector &account_ids, FieldMaskFlags mask, const std::vector &field_names); - std::optional tryGetFieldAccess(std::uint64_t account_id, const Member &) const; - std::optional tryGetFieldAccess(std::uint64_t account_id, const MemberLoc &) const; + std::optional tryGetFieldAccessByMember(std::uint64_t account_id, const Member &) const; + std::optional tryGetFieldAccessByMemberLoc(std::uint64_t account_id, const MemberLoc &) const; std::optional tryGetFieldAccess(std::uint64_t account_id, const char *field_name) const; + std::optional tryGetFieldAccessByName(std::uint64_t account_id, const char *field_name) const; std::vector > getFieldAccess(std::uint64_t account_id) const; std::uint32_t getFieldOffsetRange() const; From 0c6ba5accd4cbd796d1c4b270e602729f937cb3c Mon Sep 17 00:00:00 2001 From: Adrian Zawadzki Date: Tue, 19 May 2026 21:37:55 +0200 Subject: [PATCH 3/3] feature(field_protections): added init and loads integration --- dbzero/dbzero/initialization.py | 10 ++++- python_tests/test_data_masking.py | 47 ++++++++++++++++++++++ python_tests/test_load.py | 62 +++++++++++++++++++++++++++-- src/dbzero/bindings/python/Memo.cpp | 44 ++++++++++++++++---- 4 files changed, 151 insertions(+), 12 deletions(-) diff --git a/dbzero/dbzero/initialization.py b/dbzero/dbzero/initialization.py index b0fb443b..4ad908cf 100644 --- a/dbzero/dbzero/initialization.py +++ b/dbzero/dbzero/initialization.py @@ -2,8 +2,9 @@ # Copyright (c) 2025 DBZero Software sp. z o.o. """dbzero initialization functions""" +from collections.abc import Mapping from typing import Any -from .dbzero import _init, open as dbzero_open +from .dbzero import _init, _init_data_masking, open as dbzero_open def init(dbzero_root: str, **kwargs: Any) -> None: """Initialize the dbzero environment in a specified directory and apply global configurations. @@ -29,6 +30,7 @@ def init(dbzero_root: str, **kwargs: Any) -> None: * cache_size (int, default 2 GiB) for main object cache size in bytes * lang_cache_size (int, default 1024) for language model data cache size * lock_flags (dict) to configure locking behavior when opening the prefix in read-write mode + * data_masking (dict) to initialize data masking via _init_data_masking Lock flags (dict): * blocking (bool, default False) wait when trying to acquire the lock @@ -55,3 +57,9 @@ def init(dbzero_root: str, **kwargs: Any) -> None: if "prefix" in kwargs: open_mode = "rw" if kwargs.get("read_write", True) else "r" dbzero_open(kwargs["prefix"], open_mode=open_mode) + + if "data_masking" in kwargs: + data_masking = kwargs["data_masking"] + if not isinstance(data_masking, Mapping): + raise TypeError("data_masking must be a mapping") + _init_data_masking(**data_masking) diff --git a/python_tests/test_data_masking.py b/python_tests/test_data_masking.py index 67ed5db2..529cdd81 100644 --- a/python_tests/test_data_masking.py +++ b/python_tests/test_data_masking.py @@ -2,16 +2,24 @@ # Copyright (c) 2025 DBZero Software sp. z o.o. from contextvars import ContextVar +from dataclasses import dataclass import pytest import dbzero as db0 +from .conftest import DB0_DIR account_id = ContextVar("account_id") missing_value = object() +@db0.memo(protect_fields=True) +@dataclass +class InitDataMaskingProtectedClass: + value: str + + def test_init_data_masking_prefix_scoped_lifecycle(db0_fixture): current_prefix = db0.get_current_prefix() @@ -141,3 +149,42 @@ def test_init_data_masking_allows_different_bindings_for_different_prefixes(db0_ missing_value_placeholder=object(), mode="RELEASE", ) + + +def test_init_can_initialize_workspace_data_masking(db0_fixture): + db0.close() + init_account_id = ContextVar("init_workspace_data_masking_account_id") + + db0.init( + DB0_DIR, + data_masking={ + "context_var": init_account_id, + "mode": "DEBUG", + }, + ) + db0.open("init-workspace-data-masking") + init_account_id.set(-2) + + obj = InitDataMaskingProtectedClass("visible") + + assert obj.value == "visible" + + +def test_init_can_initialize_prefix_data_masking_after_opening_prefix(db0_fixture): + db0.close() + init_account_id = ContextVar("init_prefix_data_masking_account_id") + + db0.init( + DB0_DIR, + prefix="init-prefix-data-masking", + data_masking={ + "context_var": init_account_id, + "prefix": "init-prefix-data-masking", + "mode": "DEBUG", + }, + ) + init_account_id.set(-2) + + obj = InitDataMaskingProtectedClass("visible") + + assert obj.value == "visible" diff --git a/python_tests/test_load.py b/python_tests/test_load.py index 8dadc394..7c3d3359 100644 --- a/python_tests/test_load.py +++ b/python_tests/test_load.py @@ -3,10 +3,37 @@ import pytest import dbzero as db0 +from contextvars import ContextVar +from dataclasses import dataclass from .memo_test_types import (MemoTestClass, MemoTestThreeParamsClass, MemoTestCustomLoadClass, MemoTestCustomLoadClassWithParams, MemoTestSingleton) +@db0.enum(values=["CREATE", "READ", "UPDATE", "DELETE"]) +class LoadFieldAccess: + pass + + +@db0.memo(protect_fields=True) +@dataclass +class LoadProtectedClass: + name: str + value: int + note: str + + +@db0.memo(protect_fields=True) +@dataclass +class LoadProtectedBaseClass: + base_value: str + + +@db0.memo +@dataclass +class LoadProtectedDerivedClass(LoadProtectedBaseClass): + derived_value: str + + def test_load_py_string(): assert db0.load("abc") == "abc" @@ -48,6 +75,36 @@ def test_load_memo_types(db0_fixture): assert db0.load(memo) == {"value": "string"} +def test_load_protected_memo_only_loads_readable_fields(db0_fixture): + account_id = ContextVar("load_protected_account_id") + memo = LoadProtectedClass("alpha", 7, "private") + db0.set_field_access(LoadProtectedClass, 123, (LoadFieldAccess.READ,), "name", "value") + db0._init_data_masking(account_id) + account_id.set(123) + + assert db0.load(memo) == {"name": "alpha", "value": 7} + + +def test_load_protected_memo_uses_inherited_read_masks(db0_fixture): + account_id = ContextVar("load_protected_inherited_account_id") + memo = LoadProtectedDerivedClass("base", "derived") + db0.set_field_access(LoadProtectedBaseClass, 123, (LoadFieldAccess.READ,), "base_value") + db0._init_data_masking(account_id) + account_id.set(123) + + assert db0.load(memo) == {"base_value": "base"} + + +def test_load_protected_memo_applies_exclude_after_read_mask(db0_fixture): + account_id = ContextVar("load_protected_exclude_account_id") + memo = LoadProtectedClass("alpha", 7, "private") + db0.set_field_access(LoadProtectedClass, 123, (LoadFieldAccess.READ,), "name", "value") + db0._init_data_masking(account_id) + account_id.set(123) + + assert db0.load(memo, exclude=["value"]) == {"name": "alpha"} + + def test_load_db0_list(db0_fixture): Colors = db0.enum("Colors", ["RED", "GREEN", "BLUE"]) cut = ["1", 2 , Colors.GREEN] @@ -312,11 +369,10 @@ def test_load_all_shallow(db0_fixture): loaded = db0.load_all(obj) # NOTE: load_all ignores custom __load__ method at the topmost level assert loaded == {"first": "one", "second": "two", "third": "three"} - - + + def test_load_all_deep(db0_fixture): obj = ObjectWithCustomLoad("one", "two", ObjectWithCustomLoad("three", "four", "five")) loaded = db0.load_all(obj) # NOTE: load_all ignores custom __load__ method at the topmost level only assert loaded == {"first": "one", "second": "two", "third": {"first": "three"}} - \ No newline at end of file diff --git a/src/dbzero/bindings/python/Memo.cpp b/src/dbzero/bindings/python/Memo.cpp index 93501c62..4a920a4c 100644 --- a/src/dbzero/bindings/python/Memo.cpp +++ b/src/dbzero/bindings/python/Memo.cpp @@ -1389,20 +1389,48 @@ namespace db0::python (const std::string &key, PyTypes::ObjectSharedPtr) { auto key_obj = Py_OWN(PyUnicode_FromString(key.c_str())); - auto attr = Py_OWN(PyAPI_MemoObject_getattro(memo_obj, *key_obj)); - if (!attr) { + if (!key_obj) { has_error = true; return false; } - - if (py_exclude == nullptr || py_exclude == Py_None || PySequence_Contains(py_exclude, *key_obj) == 0) { - auto res = Py_OWN(tryLoad(*attr, kwargs, nullptr, load_stack_ptr, false)); - if (!res) { + + if (py_exclude != nullptr && py_exclude != Py_None) { + int contains = PySequence_Contains(py_exclude, *key_obj); + if (contains < 0) { has_error = true; - } else { - PySafeDict_SetItemString(*py_result, key.c_str(), res); + return false; + } + if (contains == 1) { + return true; + } + } + + auto &memo_type = memo_obj->ext().getType(); + if (memo_type.isProtectFields()) { + auto member_loc = memo_obj->ext().findField(key.c_str()); + if (!checkProtectedFieldAccess( + memo_obj, db0::object_model::FieldMaskOptions::READ, member_loc, key.c_str() + )) { + if (PyErr_Occurred()) { + has_error = true; + return false; + } + return true; } } + + auto attr = Py_OWN(PyAPI_MemoObject_getattro(memo_obj, *key_obj)); + if (!attr) { + has_error = true; + return false; + } + + auto res = Py_OWN(tryLoad(*attr, kwargs, nullptr, load_stack_ptr, false)); + if (!res) { + has_error = true; + } else { + PySafeDict_SetItemString(*py_result, key.c_str(), res); + } return !has_error; }); if (has_error) {