Skip to content

Commit

Permalink
Setting Versionsing (#45)
Browse files Browse the repository at this point in the history
* documentation

* in progress

* .

* ready for PR

* lint

* cr
  • Loading branch information
bentheiii authored Jan 3, 2022
1 parent 31d44cf commit 84130a5
Show file tree
Hide file tree
Showing 24 changed files with 1,683 additions and 604 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
### Removed
* old api endpoint POST /api/v1/rules/query has been removed and replaced with GET /api/v1/rules/query
### Changed
* the rename api endpoint has been changed to PUT /api/v1/<name>/name
* the rename api endpoint has been changed to PUT /api/v1/<name>/name.
* the method of the endpoint /api/v1/rules/search has been changed to GET.
* All setting now must have a default value.
* Setting declarations are now versioned.
### Deprecated
* The api endpoint PATCH /api/v1/rules/<rule> to change a rule's value is now deprecated, new users
should use PUT /api/v1/rules/<rule>/value
Expand All @@ -13,6 +15,7 @@
* documentation
* The api endpoint PUT /api/v1/rules/<rule>/value to change a rule's value
* The api endpoint GET /api/v1/rules/query to query rules (replaces the old query endpoint)
* POST /api/v1/rules now returns the rule location in the header
### Fixed
* A bug where patching a context feature's index using "to_before" would use the incorrect target.
### Internal
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Welcome to the Heksher documentation!
running
setting_types
api
setting_versions
setting_aliases
cookbook
libraries
Expand Down
1 change: 0 additions & 1 deletion docs/setting_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ hold any dictionary mapping strings to dictionaries that map strings to integers

Type Order
----------

Setting types have a partial ordering over them. This when we want to safely change a setting's type. We say that type
A is a supertype of type B if every value that can be stored in type B can also be stored in type A. This will help us
when :ref:`declaring settings <api:POST /api/v1/settings/declare>`.
Expand Down
53 changes: 53 additions & 0 deletions docs/setting_versions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Setting Versions
===================

Over time, attributes of settings may change. These changes might be metadata differences, setting type changes, or
even wholesale renaming of the setting. These changes must be as backwards compatible as possible, so that users using a
setting's older attributes might still function. At the same time, we want to make sure that these older declarations
don't regress the settings back to their old attributes. We consolidate those two needs with setting versions.

Whenever a setting is declared, it is declared with a version (the default version is ``1.0``).If the setting does not
yet exist, it is created (in this case, we expect the version to be ``1.0``). If the setting already exists, we check
the latest declared version of the setting.

* If the latest declared version is the same as the current version, we assert that the values are the same as the
latest declaration. If the assertion fails, we inform the user of an attribute mismatch.
* If the latest declared version is higher than the declaration version, we inform the user that they are declaring with
outdated attributes.
.. warning::

Differing attributes are not checked for older versions. If a user purposely declares a setting with an older
version but with different attributes then those used for that version, no issue will be reported (but this will not
affect other users whatsoever). This behavior might change in the future.

* If the latest declared version is lower than the declaration version, we update the setting attributes to reflect the
new declaration.


Note that not all changes are automatically accepted. If the new version is higher than the current version only in the
second number (what we call a **minor change**), only the following changes are accepted:

* Changing metadata.
* Changing the setting type to a :ref:`subtype <setting_types:Type Order>` of the current setting type.
* Renaming the setting (while defining the old name as an alias).
* Removing a configurable feature that no rule of the setting is configured by.
* Changing a default value.

.. note::

These are changes that we expect to be fully backwards compatible. i.e. if a setting is declared with an older
version, we expect to be fully functional.

If the new version is higher than the current version in the first number (what we call a **major change**), we accept
the following changes:

* Changing metadata.
* Changing the setting type.
* Renaming the setting (while defining the old name as an alias).
* Changing configurable features.
* Changing a default value.

There are some changes that are never acceptable, as they would break the logic of the application. These are:

* Changing a setting type to a value that does not accept the value of at least one rule of the setting.
* Removing configurable features that are matched by at least one rule of the setting.
21 changes: 13 additions & 8 deletions heksher/api/v1/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from heksher.api.v1.util import ORJSONModel, PydanticResponse, application, handle_etag, router as v1_router
from heksher.api.v1.validation import ContextFeatureName, ContextFeatureValue, MetadataKey, SettingName
from heksher.app import HeksherApp
from heksher.setting import Setting

router = APIRouter(prefix='/rules')
logger = getLogger(__name__)
Expand Down Expand Up @@ -48,7 +47,8 @@ async def search_rule(app: HeksherApp = application,
"""
Get the ID of a rule with specific conditions.
"""
canon_setting = await app.db_logic.get_setting(setting, include_metadata=False) # for aliasing
canon_setting = await app.db_logic.get_setting(setting, include_metadata=False, include_configurable_features=False,
include_aliases=False) # for aliasing
if not canon_setting:
return Response(status_code=status.HTTP_404_NOT_FOUND)
feature_values_dict: Dict[str, str] = dict(part.split(':') for part in feature_values.split(',')) # type: ignore
Expand All @@ -62,7 +62,7 @@ class AddRuleInput(ORJSONModel):
setting: SettingName = Field(description="the setting name the rule should apply to")
feature_values: Dict[ContextFeatureName, ContextFeatureValue] = \
Field(description="the exact-match conditions of the rule")
value: Any = Field(description="the value of the setting in contexts that match the rule")
value: Any = Field(..., description="the value of the setting in contexts that match the rule")
metadata: Dict[MetadataKey, Any] = Field(default_factory=dict, description="additional metadata of the rule")

@validator('feature_values')
Expand All @@ -82,7 +82,8 @@ async def add_rule(input: AddRuleInput, app: HeksherApp = application):
"""
Add a rule, and get its ID.
"""
setting: Optional[Setting] = await app.db_logic.get_setting(input.setting, include_metadata=False)
setting = await app.db_logic.get_setting(input.setting, include_metadata=False, include_configurable_features=True,
include_aliases=False)
if not setting:
return PlainTextResponse(f'setting not found with name {input.setting}',
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
Expand All @@ -101,11 +102,12 @@ async def add_rule(input: AddRuleInput, app: HeksherApp = application):

new_id = await app.db_logic.add_rule(setting.name, input.value, input.metadata, input.feature_values)

return AddRuleOutput(rule_id=new_id)
return PydanticResponse(AddRuleOutput(rule_id=new_id),
headers={'Location': f'/{new_id}'}, status_code=status.HTTP_201_CREATED)


class PatchRuleInput(ORJSONModel):
value: Any = Field(description="the value of the setting in contexts that match the rule")
value: Any = Field(..., description="the value of the setting in contexts that match the rule")


@router.patch('/{rule_id}', status_code=status.HTTP_204_NO_CONTENT, response_class=Response)
Expand All @@ -118,7 +120,8 @@ async def patch_rule(rule_id: int, input: PatchRuleInput, app: HeksherApp = appl
if not rule:
return PlainTextResponse(status_code=status.HTTP_404_NOT_FOUND)

setting = await app.db_logic.get_setting(rule.setting, include_metadata=False)
setting = await app.db_logic.get_setting(rule.setting, include_metadata=False, include_aliases=False,
include_configurable_features=False)
assert setting

if not setting.type.validate(input.value):
Expand Down Expand Up @@ -191,7 +194,9 @@ async def query_rules(request: Request, app: HeksherApp = application,
" response"),
):
if raw_settings is None:
settings = await app.db_logic.get_all_settings_names()
settings = [spec.name for spec in await app.db_logic.get_all_settings(include_configurable_features=False,
include_aliases=False,
include_metadata=False)]
elif not raw_settings:
settings = []
else:
Expand Down
2 changes: 1 addition & 1 deletion heksher/api/v1/rules_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async def replace_rule_metadata(rule_id: int, input: InputRuleMetadata, app: Hek


class PutRuleMetadataKey(ORJSONModel):
value: Any = Field(description="the new value of the given key and rule in the rule's metadata")
value: Any = Field(..., description="the new value of the given key and rule in the rule's metadata")


@router.put('/{rule_id}/metadata/{key}', status_code=status.HTTP_204_NO_CONTENT, response_class=Response)
Expand Down
Loading

0 comments on commit 84130a5

Please sign in to comment.