Skip to content

Commit

Permalink
[Chore] Factor out validators to separate module
Browse files Browse the repository at this point in the history
Problem: wizard_structure contains too much definitions, which
became hard to maintain.

Solution: Put all validators under `validators` module. Strip
`_validator` from its functions, since they are supposed to be called
with module qualified. Add typing information to the `Validator` class.
  • Loading branch information
krendelhoff2 committed Sep 22, 2023
1 parent 272067a commit f6db4f3
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 174 deletions.
24 changes: 13 additions & 11 deletions baking/src/tezos_baking/tezos_setup_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

from tezos_baking.wizard_structure import *
from tezos_baking.util import *
from tezos_baking.validators import Validator
import tezos_baking.validators as validators

# Global options

Expand Down Expand Up @@ -189,7 +191,7 @@ def get_node_version_hash():
"Keep in mind that you must select the test network (e.g. ghostnet)\n"
"if you plan on baking with a faucet JSON file.\n",
options=networks,
validator=Validator(enum_range_validator(networks)),
validator=Validator(validators.enum_range(networks)),
)

service_mode_query = Step(
Expand All @@ -199,7 +201,7 @@ def get_node_version_hash():
"on different networks.\nSometimes, however, you might want to only run the Tezos node.\n"
"When this option is chosen, this wizard will help you bootstrap the Tezos node only.",
options=modes,
validator=Validator(enum_range_validator(modes)),
validator=Validator(validators.enum_range(modes)),
)

systemd_mode_query = Step(
Expand All @@ -208,7 +210,7 @@ def get_node_version_hash():
help="Starting the service will make it available just for this session, great\n"
"if you want to experiment. Enabling it will make it start on every boot.",
options=systemd_enable,
validator=Validator(enum_range_validator(systemd_enable)),
validator=Validator(validators.enum_range(systemd_enable)),
)

liquidity_toggle_vote_query = Step(
Expand All @@ -219,7 +221,7 @@ def get_node_version_hash():
"\nYou can read more about this in the here:\n"
"https://tezos.gitlab.io/active/liquidity_baking.html",
options=toggle_vote_modes,
validator=Validator(enum_range_validator(toggle_vote_modes)),
validator=Validator(validators.enum_range(toggle_vote_modes)),
)

# We define this step as a function to better tailor snapshot options to the chosen history mode
Expand Down Expand Up @@ -263,7 +265,7 @@ def get_snapshot_mode_query(config):
"that is sufficient for baking. You can read more about other Tezos node history modes here:\n"
"https://tezos.gitlab.io/user/history_modes.html#history-modes",
options=import_modes,
validator=Validator(enum_range_validator(import_modes)),
validator=Validator(validators.enum_range(import_modes)),
)


Expand All @@ -273,15 +275,15 @@ def get_snapshot_mode_query(config):
help="You have indicated wanting to import the snapshot from a file.\n"
"You can download the snapshot yourself e.g. from XTZ-Shots or Tezos Giganode Snapshots.",
default=None,
validator=Validator([required_field_validator, filepath_validator]),
validator=Validator([validators.required_field, validators.filepath]),
)

provider_url_query = Step(
id="provider_url",
prompt="Provide the url of the snapshot provider.",
help="You have indicated wanting to fetch the snapshot from a custom provider.\n",
default=None,
validator=Validator([required_field_validator, reachable_url_validator()]),
validator=Validator([validators.required_field, validators.reachable_url()]),
)

snapshot_url_query = Step(
Expand All @@ -290,7 +292,7 @@ def get_snapshot_mode_query(config):
help="You have indicated wanting to import the snapshot from a custom url.\n"
"You can use e.g. links to XTZ-Shots or Marigold resources.",
default=None,
validator=Validator([required_field_validator, reachable_url_validator()]),
validator=Validator([validators.required_field, validators.reachable_url()]),
)

snapshot_sha256_query = Step(
Expand All @@ -309,7 +311,7 @@ def get_snapshot_mode_query(config):
"You can read more about different nodes history modes here:\n"
"https://tezos.gitlab.io/user/history_modes.html",
options=history_modes,
validator=Validator(enum_range_validator(history_modes)),
validator=Validator(validators.enum_range(history_modes)),
)

# We define the step as a function to disallow choosing json baking on mainnet
Expand All @@ -323,7 +325,7 @@ def get_key_mode_query(modes):
"If you want to test baking with a faucet file, "
"make sure you have chosen a test network like " + list(networks.keys())[1],
options=modes,
validator=Validator(enum_range_validator(modes)),
validator=Validator(validators.enum_range(modes)),
)


Expand All @@ -337,7 +339,7 @@ def get_key_mode_query(modes):
prompt="Do you want to proceed with this snapshot anyway?",
help="It's possible, but not recommended, to ignore the sha256 mismatch and use this snapshot anyway.",
options=ignore_hash_mismatch_options,
validator=Validator(enum_range_validator(ignore_hash_mismatch_options)),
validator=Validator(validators.enum_range(ignore_hash_mismatch_options)),
)


Expand Down
24 changes: 13 additions & 11 deletions baking/src/tezos_baking/tezos_voting_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

from tezos_baking.wizard_structure import *
from tezos_baking.util import *
from tezos_baking.validators import Validator
import tezos_baking.validators as validators

# Global options

Expand Down Expand Up @@ -89,7 +91,7 @@ def wait_for_ledger_app(app_name, client_dir):
prompt="Provide the hash for your newly submitted proposal.",
default=None,
help="The format is 'P[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{50}'",
validator=Validator([required_field_validator, protocol_hash_validator]),
validator=Validator([validators.required_field, validators.protocol_hash]),
)

# We define this step as a function since the corresponding step requires that we get the
Expand All @@ -111,8 +113,8 @@ def get_proposal_period_hash(hashes):
options=hashes + extra_options,
validator=Validator(
[
required_field_validator,
enum_range_validator(hashes + extra_options),
validators.required_field,
validators.enum_range(hashes + extra_options),
]
),
)
Expand All @@ -126,15 +128,15 @@ def get_proposal_period_hash(hashes):
default=None,
options=ballot_outcomes,
validator=Validator(
[required_field_validator, enum_range_validator(ballot_outcomes)]
[validators.required_field, validators.enum_range(ballot_outcomes)]
),
)


def get_node_rpc_endpoint_query(network, default=None):
url_path = "chains/main/blocks/head/header"
node_is_alive = lambda host: url_is_reachable(mk_full_url(host, url_path))
custom_url_validator = reachable_url_validator(url_path)
custom_url_validator = validators.reachable_url(url_path)

relevant_nodes = {
url: provider
Expand All @@ -152,10 +154,10 @@ def get_node_rpc_endpoint_query(network, default=None):
options=relevant_nodes,
validator=Validator(
[
required_field_validator,
or_validator(
enum_range_validator(relevant_nodes),
custom_url_validator,
validators.required_field,
validators.any_of(
validators.enum_range(relevant_nodes),
validators.custom_url,
),
]
),
Expand All @@ -168,7 +170,7 @@ def get_node_rpc_endpoint_query(network, default=None):
help="The baker's alias will be used by octez-client to vote. If you have baking set up\n"
"through systemd services, the address is usually 'baker' by default.",
default=None,
validator=Validator([required_field_validator]),
validator=Validator([validators.required_field]),
)

# We define the step as a function to disallow choosing json
Expand All @@ -180,7 +182,7 @@ def get_key_mode_query(modes):
"that will be used for voting. You will only need to import the key\n"
"once unless you'll want to change the key.",
options=modes,
validator=Validator(enum_range_validator(modes)),
validator=Validator(validators.enum_range(modes)),
)


Expand Down
174 changes: 174 additions & 0 deletions baking/src/tezos_baking/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# SPDX-FileCopyrightText: 2023 Oxhead Alpha
# SPDX-License-Identifier: LicenseRef-MIT-OA

"""
Contains user input validators
"""

import re
import os

from .util import *


def enum_range(options):
def _validator(input):
intrange = list(map(str, range(1, len(options) + 1)))
if input not in intrange and input not in options:
raise ValueError(
"Please choose one of the provided values or use their respective numbers."
)
try:
opt = int(input) - 1
except:
return input
else:
opts = options
if isinstance(options, dict):
opts = list(options.keys())
return opts[opt]

return _validator


def dirpath(input):
if input and not os.path.isdir(input):
raise ValueError("Please input a valid path to a directory.")
return input


def filepath(input):
if input and not os.path.isfile(input):
raise ValueError("Please input a valid file path.")
return input


def reachable_url(suffix=None):
def _validator(input):
full_url = mk_full_url(input, suffix)
if url_is_reachable(full_url):
return full_url
else:
raise ValueError(f"{full_url} is unreachable. Please input a valid URL.")

return _validator


def required_field(input):
if not input.strip():
raise ValueError("Please provide this required option.")
return input


# The input has to be valid to at least one of the two passed validators.
def any_of(validator1, validator2):
def _validator(input):
try:
return validator1(input)
except:
return validator2(input)

return _validator


# Runs the input through the passed validator, allowing for possible alteration,
# but doesn't raise an exception if it doesn't validate to allow for custom options, too.
def or_custom(validator):
def _validator(input):
result = input
try:
result = validator(input)
except:
pass
return result

return _validator


# To be validated, the input should adhere to the Tezos secret key format:
# {encrypted, unencrypted}:<base58 encoded string with length 54 or 88>
def secret_key(input):
match = re.match(secret_key_regex.decode("utf-8"), input.strip())
if not bool(match):
raise ValueError(
"The input doesn't match the format for a Tezos secret key: "
"{{encrypted, unencrypted}:<base58 encoded string with length 54 or 88>}"
"\nPlease check the input and try again."
)
return input


# To be validated, the input should adhere to the Tezos secret key format:
# {encrypted, unencrypted}:<base58 encoded string with length 54 or 88>
def unencrypted_secret_key(input):
match = re.match(unencrypted_secret_key_regex.decode("utf-8"), input.strip())
if not bool(match):
raise ValueError(
"The input doesn't match the format for a Tezos secret key: "
"{{encrypted, unencrypted}:<base58 encoded string with length 54 or 88>}"
"\nPlease check the input and try again."
)
return input


# To be validated, the input should adhere to the derivation path format:
# [0-9]+h/[0-9]+h
def derivation_path(input):
derivation_path_regex_str = "[0-9]+h\/[0-9]+h"
match = re.match(derivation_path_regex_str, input.strip())
if not bool(match):
raise ValueError(
"The input doesn't match the format for a derivation path: "
+ derivation_path_regex_str
+ "\nPlease check the input and try again."
)
return input


# To be validated, the input should adhere to the signer URI format:
# (tcp|unix|https|http)://<host address>/tz[123]\w{33}
def signer_uri(input):
match = re.match(signer_uri_regex.decode("utf-8"), input.strip())
if not bool(match):
raise ValueError(
"The input doesn't match the format for a remote signer URI: "
+ "(tcp|unix|https|http)://<host address>/<public key address>"
+ "\nPlease check the input and try again."
)
return input


# To be validated, the input should adhere to the protocol hash format:
# <base58 encoded string with length 51 starting with P>
def protocol_hash(input):
proto_hash_regex_str = protocol_hash_regex.decode("utf-8")
match = re.match(proto_hash_regex_str, input.strip())
if not bool(match):
raise ValueError(
"The input doesn't match the format for a protocol hash: "
+ proto_hash_regex_str
+ "\nPlease check the input and try again."
)
return input


from dataclasses import dataclass
from typing import *

ValidatorFunction = Callable[[str], str]


@dataclass
class Validator:
validator: Union[
ValidatorFunction,
List[ValidatorFunction],
] = lambda x: x

def validate(self, input: str):
if isinstance(self.validator, list):
for v in self.validator:
input = v(input)
return input
else:
return self.validator(input)
Loading

0 comments on commit f6db4f3

Please sign in to comment.