Repository: boto/botocore
Pull request #2737 (2022-08-15 17:37:28)

Endpoint Provider for botocore
This pull request implements a new interface for botocore `EndpointProvider`. As part of the Endpoints 2.0 initiative across the AWS SDK organization, it is responsible for reading a given service's endpoint ruleset and "resolving" aka determining an endpoint to return based on a set of parameters (i.e. region, bucket name etc.). 

This is done through the main API `resolve_endpoint`. It expects input parameters in the form of a dictionary and uses that to create a `Ruleset` object. This object houses the specification for each expected input parameter and rules that the provider will evaluate given those parameters. These rule objects come in three distinct types: `EndpointRule`, `TreeRule` and `ErrorRule`.

These three types of rules share a lot of common behavior. They all have a `conditions` property, which are a list of functions (can be empty but always present). These functions will evaluate to a truthy or falsy value depending on the input parameters given to the provider. All conditions in a rule must be truthy for it to be considered a match. Endpoint and error rules are terminal in that if their conditions are met, the evaluation will immediately be complete and return/raise. A tree rule is different in that it also contains a set of rules which have their own conditions that need to evaluate to a truthy value for it to be a match. However, only one of a tree rule's subordinate rules need to be a match for it itself to still be considered one as well. A tree rule is never terminal and therefore will never be returned to a provider.  If a rule's conditions are not met, the evaluation immediately terminates and proceeds to the next rule if there are any. An error is raised if no match is found.

The `test_endpoint_resolution` test functions run tests for all of the automatically generated test cases. The rest of the tests are handwritten to fill out any code that wasn't covered by the generated ones.

codecov-commenter: # [Codecov](https://codecov.io/gh/boto/botocore/pull/2737?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=boto) Report
> Merging [#2737](https://codecov.io/gh/boto/botocore/pull/2737?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=boto) (fc38e96) into [develop](https://codecov.io/gh/boto/botocore/commit/474e7a23d0fd178790579638cec9123d7e92d10b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=boto) (474e7a2) will **decrease** coverage by `0.03%`.
> The diff coverage is `100.00%`.

```diff
@@             Coverage Diff             @@
##           develop    #2737      +/-   ##
===========================================
- Coverage    95.25%   95.22%   -0.04%     
===========================================
  Files           60       61       +1     
  Lines        12278    12560     +282     
===========================================
+ Hits         11696    11960     +264     
- Misses         582      600      +18     
```


| [Impacted Files](https://codecov.io/gh/boto/botocore/pull/2737?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=boto) | Coverage Δ | |
|---|---|---|
| [botocore/compat.py](https://codecov.io/gh/boto/botocore/pull/2737/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=boto#diff-Ym90b2NvcmUvY29tcGF0LnB5) | `92.66% <100.00%> (+0.04%)` | :arrow_up: |
| [botocore/httpsession.py](https://codecov.io/gh/boto/botocore/pull/2737/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=boto#diff-Ym90b2NvcmUvaHR0cHNlc3Npb24ucHk=) | `91.33% <0.00%> (-0.40%)` | :arrow_down: |
| [botocore/utils.py](https://codecov.io/gh/boto/botocore/pull/2737/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=boto#diff-Ym90b2NvcmUvdXRpbHMucHk=) | `93.34% <0.00%> (-0.27%)` | :arrow_down: |
| [botocore/endpoints\_v2.py](https://codecov.io/gh/boto/botocore/pull/2737/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=boto#diff-Ym90b2NvcmUvZW5kcG9pbnRzX3YyLnB5) | `95.18% <0.00%> (ø)` | |
| [botocore/exceptions.py](https://codecov.io/gh/boto/botocore/pull/2737/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=boto#diff-Ym90b2NvcmUvZXhjZXB0aW9ucy5weQ==) | `99.55% <0.00%> (+0.01%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=boto). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=boto)

jonemo: Why are the "standard library" functions methods of `BaseRule`? These function don't operate on the rule object itself or use any of its properties, as can be seen at a glance because there aren't any usages of `self`. So they could equally well be standalone functions or staticmethods. The one exception is `aws_partition` which needs the partition information, but that information too doesn't need to be part of each and every rule object because it's the same for all rules. 

I am mainly asking this because I am wondering how many of the standard library calls may be repeated, possibly even within a single evaluation of a single ruleset. Would we see big performance improvements by adding caching to them? And if yes, then that only works if we have a single instance of each function instead of the many we currently have.
dlm6693: > Why are the "standard library" functions methods of `BaseRule`? These function don't operate on the rule object itself or use any of its properties, as can be seen at a glance because there aren't any usages of `self`. So they could equally well be standalone functions or staticmethods. The one exception is `aws_partition` which needs the partition information, but that information too doesn't need to be part of each and every rule object because it's the same for all rules.
> 
> I am mainly asking this because I am wondering how many of the standard library calls may be repeated, possibly even within a single evaluation of a single ruleset. Would we see big performance improvements by adding caching to them? And if yes, then that only works if we have a single instance of each function instead of the many we currently have.

@jonemo I mentioned this a few weeks ago, but never got any feedback on it so left them as-is. I put them there mostly for convenience since they operate on a rule's conditions, but yes they definitely can exist independently. The only issue would be turning the function name of an input string into the function object itself, but we could just add a mapping for that. I'm not sure of the performance benefit though, but happy to do some manual testing on that.

Patch:
diff --git a/botocore/compat.py b/botocore/compat.py
index bfb5df33c..1091b4167 100644
--- a/botocore/compat.py
+++ b/botocore/compat.py
@@ -307,6 +307,7 @@ except ImportError:
 # Vendoring IPv6 validation regex patterns from urllib3
 # https://github.com/urllib3/urllib3/blob/7e856c0/src/urllib3/util/url.py
 IPV4_PAT = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}"
+IPV4_RE = re.compile("^" + IPV4_PAT + "$")
 HEX_PAT = "[0-9A-Fa-f]{1,4}"
 LS32_PAT = "(?:{hex}:{hex}|{ipv4})".format(hex=HEX_PAT, ipv4=IPV4_PAT)
 _subs = {"hex": HEX_PAT, "ls32": LS32_PAT}
diff --git a/botocore/endpoint_provider.py b/botocore/endpoint_provider.py
new file mode 100644
index 000000000..750b4d45b
--- /dev/null
+++ b/botocore/endpoint_provider.py
@@ -0,0 +1,687 @@
+# Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+
+"""
+NOTE: All classes and functions in this module are considered private and are
+subject to abrupt breaking changes. Please do not use them directly.
+
+To view the raw JSON that the objects in this module represent, please
+go to any `endpoint-rule-set.json` file in /botocore/data/<service>/<api version>/
+or you can look at the test files in /tests/unit/data/endpoints/valid-rules/
+"""
+
+
+import logging
+import re
+from enum import Enum
+from functools import lru_cache
+from string import Formatter
+from typing import NamedTuple
+
+from botocore import xform_name
+from botocore.compat import quote, urlparse
+from botocore.exceptions import EndpointResolutionError
+from botocore.utils import (
+    ArnParser,
+    InvalidArnException,
+    is_valid_ipv4_endpoint_url,
+    is_valid_ipv6_endpoint_url,
+    normalize_url_path,
+    percent_encode,
+)
+
+logger = logging.getLogger(__name__)
+
+TEMPLATE_STRING_RE = re.compile(r"\{[a-zA-Z#]+\}")
+GET_ATTR_RE = re.compile(r"(\w+)\[(\d+)\]")
+VALID_HOST_LABEL_RE = re.compile(
+    r"^(?!-)[a-zA-Z\d-]{1,63}(?<!-)$",
+)
+CACHE_SIZE = 100
+ARN_PARSER = ArnParser()
+STRING_FORMATTER = Formatter()
+
+
+class RuleSetStandardLibary:
+    """Rule actions to be performed by the EndpointProvider."""
+
+    def __init__(self, partitions_data):
+        self.partitions_data = partitions_data
+
+    def is_func(self, argument):
+        """Determine if an object is a function object.
+
+        :type argument: Any
+        :rtype: bool
+        """
+        return isinstance(argument, dict) and "fn" in argument
+
+    def is_ref(self, argument):
+        """Determine if an object is a reference object.
+
+        :type argument: Any
+        :rtype: bool
+        """
+        return isinstance(argument, dict) and "ref" in argument
+
+    def is_template(self, argument):
+        """Determine if an object contains a template string.
+
+        :type argument: Any
+        :rtpe: bool
+        """
+        return (
+            isinstance(argument, str)
+            and TEMPLATE_STRING_RE.search(argument) is not None
+        )
+
+    def resolve_template_string(self, value, scope_vars):
+        """Resolve and inject values into a template string.
+
+        :type value: str
+        :type scope_vars: dict
+        :rtype: str
+        """
+        result = ""
+        for literal, reference, _, _ in STRING_FORMATTER.parse(value):
+            if reference is not None:
+                template_value = scope_vars
+                template_params = reference.split("#")
+                for param in template_params:
+                    template_value = template_value[param]
+                result += f"{literal}{template_value}"
+            else:
+                result += literal
+        return result
+
+    def resolve_value(self, value, scope_vars):
+        """Return evaluated value based on type.
+
+        :type value: Any
+        :type scope_vars: dict
+        :rtype: Any
+        """
+        if self.is_func(value):
+            return self.call_function(value, scope_vars)
+        elif self.is_ref(value):
+            return scope_vars.get(value["ref"])
+        elif self.is_template(value):
+            return self.resolve_template_string(value, scope_vars)
+
+        return value
+
+    def convert_func_name(self, value):
+        """Normalize function names.
+
+        :type value: str
+        :rtype: str
+        """
+        normalized_name = f"{xform_name(value)}"
+        if normalized_name == "not":
+            normalized_name = f"_{normalized_name}"
+        return normalized_name.replace(".", "_")
+
+    def call_function(self, func_signature, scope_vars):
+        """Call the function with the resolved arguments and assign to `scope_vars`
+        when applicable.
+
+        :type func_signature: dict
+        :type scope_vars: dict
+        :rtype: Any
+        """
+        func_args = [
+            self.resolve_value(arg, scope_vars)
+            for arg in func_signature["argv"]
+        ]
+        func_name = self.convert_func_name(func_signature["fn"])
+        func = getattr(self, func_name)
+        result = func(*func_args)
+        if "assign" in func_signature:
+            assign = func_signature["assign"]
+            if assign in scope_vars:
+                raise EndpointResolutionError(
+                    msg=f"Assignment {assign} already exists in "
+                    "scoped variables and cannot be overwritten"
+                )
+            scope_vars[assign] = result
+        return result
+
+    def is_set(self, value):
+        """Evaluates whether a value is set.
+
+        :type value: Any
+        :rytpe: bool
+        """
+        return value is not None
+
+    def get_attr(self, value, path):
+        """Find an attribute within a value given a path string. The path can contain
+        the name of the attribute and an index in brackets. A period separating attribute
+        names indicates the one to the right is nested. The index will always occur at
+        the end of the path.
+
+        :type value: dict or list
+        :type path: str
+        :rtype: Any
+        """
+        for part in path.split("."):
+            match = GET_ATTR_RE.search(part)
+            if match is not None:
+                name, index = match.groups()
+                index = int(index)
+                value = value.get(name)
+                if value is None or index >= len(value):
+                    return None
+                return value[index]
+            else:
+                value = value[part]
+        return value
+
+    def format_partition_output(self, partition):
+        output = partition["outputs"]
+        output["name"] = partition["id"]
+        return output
+
+    def is_partition_match(self, region, partition):
+        matches_regex = re.match(partition["regionRegex"], region) is not None
+        return region in partition["regions"] or matches_regex
+
+    def aws_partition(self, value):
+        """Match a region string to an AWS partition.
+
+        :type value: str
+        :rtype: dict
+        """
+        if value is None:
+            return None
+
+        partitions = self.partitions_data['partitions']
+        for partition in partitions:
+            if self.is_partition_match(value, partition):
+                return self.format_partition_output(partition)
+
+        # return the default partition if no matches were found
+        aws_partition = partitions[0]
+        return self.format_partition_output(aws_partition)
+
+    def aws_parse_arn(self, value):
+        """Parse and validate string for ARN components.
+
+        :type value: str
+        :rtype: dict
+        """
+        if value is None or not value.startswith("arn:"):
+            return None
+
+        try:
+            arn_dict = ARN_PARSER.parse_arn(value)
+        except InvalidArnException:
+            return None
+
+        # partition, resource, and service are required
+        if not all(
+            (arn_dict["partition"], arn_dict["service"], arn_dict["resource"])
+        ):
+            return None
+
+        arn_dict["accountId"] = arn_dict.pop("account")
+
+        resource = arn_dict.pop("resource")
+        delimiter = ":" if ":" in resource else "/"
+        arn_dict["resourceId"] = resource.split(delimiter)
+
+        return arn_dict
+
+    def is_valid_host_label(self, value, allow_subdomains):
+        """Evaluates whether a value is a valid host label per
+        RFC 1123. If allow_subdomains is True, split on `.` and validate
+        each component separately.
+
+        :type value: str
+        :type allow_subdomains: bool
+        :rtype: bool
+        """
+        if value is None or allow_subdomains is False and value.count(".") > 0:
+            return False
+
+        if allow_subdomains is True:
+            return all(
+                self.is_valid_host_label(label, False)
+                for label in value.split(".")
+            )
+
+        return VALID_HOST_LABEL_RE.match(value) is not None
+
+    def string_equals(self, value1, value2):
+        """Evaluates two string values for equality.
+
+        :type value1: str
+        :type value2: str
+        :rtype: bool
+        """
+        if not all(isinstance(val, str) for val in (value1, value2)):
+            msg = f"Both values must be strings, not {type(value1)} and {type(value2)}."
+            raise EndpointResolutionError(msg=msg)
+        return value1 == value2
+
+    def uri_encode(self, value):
+        """Perform percent-encoding on an input string.
+
+        :type value: str
+        :rytpe: str
+        """
+        if value is None:
+            return None
+
+        return percent_encode(value)
+
+    def parse_url(self, value):
+        """Parse a URL string into components.
+
+        :type value: str
+        :rtype: dict
+        """
+        if value is None:
+            return None
+
+        url_components = urlparse(value)
+        try:
+            # url_parse may assign non-integer values to
+            # `port` and will fail when accessed.
+            url_components.port
+        except ValueError:
+            return None
+
+        scheme = url_components.scheme
+        query = url_components.query
+        # URLs with queries are not supported
+        if scheme not in ("https", "http") or len(query) > 0:
+            return None
+
+        path = url_components.path
+        normalized_path = quote(normalize_url_path(path))
+        if not normalized_path.endswith("/"):
+            normalized_path = f"{normalized_path}/"
+
+        return {
+            "scheme": scheme,
+            "authority": url_components.netloc,
+            "path": path,
+            "normalizedPath": normalized_path,
+            "isIp": is_valid_ipv4_endpoint_url(value)
+            or is_valid_ipv6_endpoint_url(value),
+        }
+
+    def boolean_equals(self, value1, value2):
+        """Evaluates two boolean values for equality.
+
+        :type value1: bool
+        :type value2: bool
+        :rtype: bool
+        """
+        if not all(isinstance(val, bool) for val in (value1, value2)):
+            msg = f"Both arguments must be bools, not {type(value1)} and {type(value2)}."
+            raise EndpointResolutionError(msg=msg)
+        return value1 is value2
+
+    def is_ascii(self, value):
+        """Evaluates if a string only contains ASCII characters.
+
+        :type value: str
+        :rtype: bool
+        """
+        try:
+            value.encode("ascii")
+            return True
+        except UnicodeEncodeError:
+            return False
+
+    def substring(self, value, start, stop, reverse):
+        """Computes a substring given the start index and end index. If `reverse` is
+        True, slice the string from the end instead.
+
+        :type value: str
+        :type start: int
+        :type end: int
+        :type reverse: bool
+        :rtype: str
+        """
+        if not isinstance(value, str):
+            msg = f"Input must be a string, not {type(value)}."
+            raise EndpointResolutionError(msg=msg)
+        if start >= stop or len(value) < stop or not self.is_ascii(value):
+            return None
+
+        if reverse is True:
+            r_start = len(value) - stop
+            r_stop = len(value) - start
+            return value[r_start:r_stop]
+
+        return value[start:stop]
+
+    def _not(self, value):
+        """A function implementation of the logical operator `not`.
+
+        :type value: Any
+        :rtype: bool
+        """
+        return not value
+
+
+class BaseRule:
+    """Base interface for individual endpoint rules."""
+
+    def __init__(self, conditions, documentation=None):
+        self.conditions = conditions
+        self.documentation = documentation
+
+    def evaluate(self, scope_vars, rule_lib):
+        raise NotImplementedError()
+
+    def evaluate_conditions(self, scope_vars, rule_lib):
+        """Determine if all conditions in a rule are met.
+
+        :type scope_vars: dict
+        :type rule_lib: RuleSetStandardLibrary
+        :rtype: bool
+        """
+        for func_signature in self.conditions:
+            result = rule_lib.call_function(func_signature, scope_vars)
+            if result is False or result is None:
+                return False
+        return True
+
+
+class RuleSetEndpoint(NamedTuple):
+    """A resolved endpoint object returned by a rule."""
+
+    url: str
+    properties: dict
+    headers: dict
+
+
+class EndpointRule(BaseRule):
+    def __init__(self, endpoint, **kwargs):
+        super().__init__(**kwargs)
+        self.endpoint = endpoint
+
+    def evaluate(self, scope_vars, rule_lib):
+        """Determine if conditions are met to provide a valid endpoint.
+
+        :type scope_vars: dict
+        :rtype: RuleSetEndpoint
+        """
+        if self.evaluate_conditions(scope_vars, rule_lib):
+            url = rule_lib.resolve_value(self.endpoint["url"], scope_vars)
+            properties = self.resolve_properties(
+                self.endpoint.get("properties", {}),
+                scope_vars,
+                rule_lib,
+            )
+            headers = self.resolve_headers(scope_vars, rule_lib)
+            return RuleSetEndpoint(
+                url=url, properties=properties, headers=headers
+            )
+
+        return None
+
+    def resolve_properties(self, properties, scope_vars, rule_lib):
+        """Traverse `properties` attribute, resolving any template strings.
+
+        :type properties: dict/list/str
+        :type scope_vars: dict
+        :type rule_lib: RuleSetStandardLibrary
+        :rtype: dict
+        """
+        if isinstance(properties, list):
+            return [
+                self.resolve_properties(prop, scope_vars, rule_lib)
+                for prop in properties
+            ]
+        elif isinstance(properties, dict):
+            return {
+                key: self.resolve_properties(value, scope_vars, rule_lib)
+                for key, value in properties.items()
+            }
+        elif rule_lib.is_template(properties):
+            return rule_lib.resolve_template_string(properties, scope_vars)
+
+        return properties
+
+    def resolve_headers(self, scope_vars, rule_lib):
+        """Iterate through headers attribute resolving all values.
+
+        :type scope_vars: dict
+        :type rule_lib: RuleSetStandardLibrary
+        :rtype: dict
+        """
+        resolved_headers = {}
+        headers = self.endpoint.get("headers", {})
+
+        for header, values in headers.items():
+            resolved_headers[header] = [
+                rule_lib.resolve_value(item, scope_vars) for item in values
+            ]
+        return resolved_headers
+
+
+class ErrorRule(BaseRule):
+    def __init__(self, error, **kwargs):
+        super().__init__(**kwargs)
+        self.error = error
+
+    def evaluate(self, scope_vars, rule_lib):
+        """If an error rule's conditions are met, raise an error rule.
+
+        :type scope_vars: dict
+        :type rule_lib: RuleSetStandardLibrary
+        :rtype: EndpointResolutionError
+        """
+        if self.evaluate_conditions(scope_vars, rule_lib):
+            error = rule_lib.resolve_value(self.error, scope_vars)
+            raise EndpointResolutionError(msg=error)
+        return None
+
+
+class TreeRule(BaseRule):
+    """A tree rule is non-terminal meaning it will never be returned to a provider.
+    Additionally this means it has no attributes that need to be resolved.
+    """
+
+    def __init__(self, rules, **kwargs):
+        super().__init__(**kwargs)
+        self.rules = [RuleCreator.create(**rule) for rule in rules]
+
+    def evaluate(self, scope_vars, rule_lib):
+        """If a tree rule's conditions are met, iterate its sub-rules
+        and return first result found.
+
+        :type scope_vars: dict
+        :type rule_lib: RuleSetStandardLibrary
+        :rtype: RuleSetEndpoint/EndpointResolutionError
+        """
+        if self.evaluate_conditions(scope_vars, rule_lib):
+            for rule in self.rules:
+                # don't share scope_vars between rules
+                rule_result = rule.evaluate(scope_vars.copy(), rule_lib)
+                if rule_result:
+                    return rule_result
+        return None
+
+
+class RuleCreator:
+
+    endpoint = EndpointRule
+    error = ErrorRule
+    tree = TreeRule
+
+    @classmethod
+    def create(cls, **kwargs):
+        """Create a rule instance from metadata.
+
+        :rtype: TreeRule/EndpointRule/ErrorRule
+        """
+        rule_type = kwargs.pop("type")
+        try:
+            rule_class = getattr(cls, rule_type)
+        except AttributeError:
+            raise EndpointResolutionError(
+                msg=f"Unknown rule type: {rule_type}. A rule must "
+                "be of type tree, endpoint or error."
+            )
+        else:
+            return rule_class(**kwargs)
+
+
+class ParameterType(Enum):
+    """Translation from `type` attribute to native Python type."""
+
+    string = str
+    boolean = bool
+
+
+class ParameterDefinition:
+    """The spec of an individual parameter defined in a RuleSet."""
+
+    def __init__(
+        self,
+        name,
+        parameter_type,
+        documentation=None,
+        builtIn=None,
+        default=None,
+        required=None,
+        deprecated=None,
+    ):
+        self.name = name
+        try:
+            self.parameter_type = getattr(
+                ParameterType, parameter_type.lower()
+            ).value
+        except AttributeError:
+            raise EndpointResolutionError(
+                msg=f"Unknown parameter type: {parameter_type}. "
+                "A parameter must be of type string or boolean."
+            )
+        self.documentation = documentation
+        self.built_in = builtIn
+        self.default = default
+        self.required = required
+        self.deprecated = deprecated
+
+    def validate_input(self, value):
+        """Perform base validation on parameter input.
+
+        :type value: Any
+        :raises: EndpointParametersError
+        """
+
+        if not isinstance(value, self.parameter_type):
+            raise EndpointResolutionError(
+                msg=f"Value ({self.name}) is the wrong "
+                f"type. Must be {self.parameter_type}."
+            )
+        if self.deprecated is not None:
+            depr_str = f"{self.name} has been deprecated."
+            msg = self.deprecated.get("message")
+            since = self.deprecated.get("since")
+            if msg:
+                depr_str += f"\n{msg}"
+            if since:
+                depr_str += f"\nDeprecated since {since}."
+            logger.info(depr_str)
+
+        return None
+
+    def process_input(self, value):
+        """Process input against spec, applying default if value is None."""
+        if value is None and self.default is not None:
+            return self.default
+        elif value is not None:
+            self.validate_input(value)
+        return value
+
+
+class RuleSet:
+    """Collection of rules to derive a routable service endpoint."""
+
+    def __init__(
+        self, version, parameters, rules, partitions, documentation=None
+    ):
+        self.version = version
+        self.parameters = self._ingest_parameter_spec(parameters)
+        self.rules = [RuleCreator.create(**rule) for rule in rules]
+        self.rule_lib = RuleSetStandardLibary(partitions)
+        self.documentation = documentation
+
+    def _ingest_parameter_spec(self, parameters):
+        return {
+            name: ParameterDefinition(
+                name,
+                spec["type"],
+                spec.get("documentation"),
+                spec.get("builtIn"),
+                spec.get("default"),
+                spec.get("required"),
+                spec.get("deprecated"),
+            )
+            for name, spec in parameters.items()
+        }
+
+    def process_input_parameters(self, input_params):
+        """Process each input parameter against its spec.
+
+        :type input_params: dict
+        """
+        for name, spec in self.parameters.items():
+            value = spec.process_input(input_params.get(name))
+            if value is not None:
+                input_params[name] = value
+        return None
+
+    def evaluate(self, input_parameters):
+        """Evaluate input parameters against rules returning first match.
+
+        :type input_parameters: dict
+        """
+        self.process_input_parameters(input_parameters)
+        for rule in self.rules:
+            evaluation = rule.evaluate(input_parameters.copy(), self.rule_lib)
+            if evaluation is not None:
+                return evaluation
+        return None
+
+
+class EndpointProvider:
+    """Derives endpoints from a RuleSet for given input parameters."""
+
+    def __init__(self, ruleset_data, partition_data):
+        self.ruleset = RuleSet(**ruleset_data, partitions=partition_data)
+
+    @lru_cache(maxsize=CACHE_SIZE)
+    def resolve_endpoint(self, **input_parameters):
+        """Match input parameters to a rule.
+
+        :type input_parameters: dict
+        :rtype: RuleSetEndpoint
+        """
+        params_for_error = input_parameters.copy()
+        endpoint = self.ruleset.evaluate(input_parameters)
+        if endpoint is None:
+            param_string = "\n".join(
+                [f"{key}: {value}" for key, value in params_for_error.items()]
+            )
+            raise EndpointResolutionError(
+                msg=f"No endpoint found for parameters:\n{param_string}"
+            )
+        return endpoint
diff --git a/botocore/exceptions.py b/botocore/exceptions.py
index 8040e4454..d2182cda0 100644
--- a/botocore/exceptions.py
+++ b/botocore/exceptions.py
@@ -798,3 +798,15 @@ class FlexibleChecksumError(BotoCoreError):
 
 class InvalidEndpointConfigurationError(BotoCoreError):
     fmt = 'Invalid endpoint configuration: {msg}'
+
+
+class EndpointProviderError(BotoCoreError):
+    """Base error for the EndpointProvider class"""
+
+    fmt = '{msg}'
+
+
+class EndpointResolutionError(EndpointProviderError):
+    """Error when input parameters resolve to an error rule"""
+
+    fmt = '{msg}'
diff --git a/botocore/utils.py b/botocore/utils.py
index 8c12120c0..fc226291e 100644
--- a/botocore/utils.py
+++ b/botocore/utils.py
@@ -37,15 +37,16 @@ import botocore.httpsession
 from botocore.compat import HEX_PAT  # noqa: F401
 from botocore.compat import IPV4_PAT  # noqa: F401
 from botocore.compat import IPV6_ADDRZ_PAT  # noqa: F401
-from botocore.compat import IPV6_ADDRZ_RE  # noqa: F401
 from botocore.compat import IPV6_PAT  # noqa: F401
 from botocore.compat import LS32_PAT  # noqa: F401
 from botocore.compat import UNRESERVED_PAT  # noqa: F401
-from botocore.compat import UNSAFE_URL_CHARS  # noqa: F401
 from botocore.compat import ZONE_ID_PAT  # noqa: F401
 from botocore.compat import (
     HAS_CRT,
+    IPV4_RE,
+    IPV6_ADDRZ_RE,
     MD5_AVAILABLE,
+    UNSAFE_URL_CHARS,
     OrderedDict,
     get_md5,
     get_tzinfo_options,
@@ -1219,6 +1220,11 @@ def is_valid_ipv6_endpoint_url(endpoint_url):
     return IPV6_ADDRZ_RE.match(hostname) is not None
 
 
+def is_valid_ipv4_endpoint_url(endpoint_url):
+    hostname = urlparse(endpoint_url).hostname
+    return IPV4_RE.match(hostname) is not None
+
+
 def is_valid_endpoint_url(endpoint_url):
     """Verify the endpoint_url is valid.
 


Test patch:
diff --git a/tests/unit/data/endpoints/test-cases/aws-region.json b/tests/unit/data/endpoints/test-cases/aws-region.json
new file mode 100644
index 000000000..0f0f40316
--- /dev/null
+++ b/tests/unit/data/endpoints/test-cases/aws-region.json
@@ -0,0 +1,32 @@
+{
+  "version": "1.0",
+  "testCases": [
+    {
+      "documentation": "basic region templating",
+      "params": {
+        "Region": "us-east-1"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://us-east-1.amazonaws.com",
+          "properties": {
+            "authSchemes": [
+              {
+                "name": "sigv4",
+                "signingRegion": "us-east-1",
+                "signingName": "serviceName"
+              }
+            ]
+          }
+        }
+      }
+    },
+    {
+      "documentation": "test case where region is unset",
+      "params": {},
+      "expect": {
+        "error": "Region must be set to resolve a valid endpoint"
+      }
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/test-cases/default-values.json b/tests/unit/data/endpoints/test-cases/default-values.json
new file mode 100644
index 000000000..84e23c868
--- /dev/null
+++ b/tests/unit/data/endpoints/test-cases/default-values.json
@@ -0,0 +1,45 @@
+{
+  "version": "1.0",
+  "testCases": [
+    {
+      "documentation": "default endpoint",
+      "params": {},
+      "expect": {
+        "endpoint": {
+          "url": "https://fips.us-west-5.amazonaws.com"
+        }
+      }
+    },
+    {
+      "documentation": "test case where FIPS is disabled",
+      "params": {
+        "UseFips": false
+      },
+      "expect": {
+        "error": "UseFips = false"
+      }
+    },
+    {
+      "documentation": "test case where FIPS is enabled explicitly",
+      "params": {
+        "UseFips": true
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://fips.us-west-5.amazonaws.com"
+        }
+      }
+    },
+    {
+      "documentation": "defaults can be overridden",
+      "params": {
+        "Region": "us-east-1"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://fips.us-east-1.amazonaws.com"
+        }
+      }
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/test-cases/eventbridge.json b/tests/unit/data/endpoints/test-cases/eventbridge.json
new file mode 100644
index 000000000..9fede903e
--- /dev/null
+++ b/tests/unit/data/endpoints/test-cases/eventbridge.json
@@ -0,0 +1,48 @@
+{
+  "version": "1.0",
+  "testCases": [
+    {
+      "documentation": "simple region endpoint",
+      "params": {
+        "region": "us-east-1"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://events.us-east-1.amazonaws.com"
+        }
+      }
+    },
+    {
+      "documentation": "basic case of endpointId",
+      "params": {
+        "region": "us-east-1",
+        "endpointId": "myendpoint"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://myendpoint.endpoint.events.amazonaws.com",
+          "properties": {
+            "authSchemes": [
+              {
+                "name": "sigv4a",
+                "signingName": "events",
+                "signingRegionSet": ["*"]
+              }
+            ]
+          }
+        }
+      }
+    },
+    {
+      "documentation": "endpointId & FIPS",
+      "params": {
+        "region": "us-east-1",
+        "endpointId": "myendpoint",
+        "useFIPSEndpoint": true
+      },
+      "expect": {
+        "error": "FIPS endpoints not supported with multi-region endpoints"
+      }
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/test-cases/fns.json b/tests/unit/data/endpoints/test-cases/fns.json
new file mode 100644
index 000000000..be0baad30
--- /dev/null
+++ b/tests/unit/data/endpoints/test-cases/fns.json
@@ -0,0 +1,50 @@
+{
+  "version": "1.0",
+  "testCases": [
+    {
+      "documentation": "test where URI is set and flows to URI and header",
+      "params": {
+        "Uri": "https://www.example.com",
+        "Arn": "arn:aws:s3:us-east-2:012345678:outpost:op-1234"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://www.example.com",
+          "headers": {
+            "x-uri": [
+              "https://www.example.com"
+            ],
+            "x-arn-region": [
+              "us-east-2"
+            ]
+          }
+        }
+      }
+    },
+    {
+      "documentation": "test where explicit error is set",
+      "params": {
+        "CustomError": "This is an error!"
+      },
+      "expect": {
+        "error": "This is an error!"
+      }
+    },
+    {
+      "documentation": "test where an ARN field is used in the error directly",
+      "params": {
+        "Arn": "arn:This is an error!:s3:us-east-2:012345678:outpost:op-1234"
+      },
+      "expect": {
+        "error": "This is an error!"
+      }
+    },
+    {
+      "documentation": "test case where no fields are set",
+      "params": {},
+      "expect": {
+        "error": "No fields were set"
+      }
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/test-cases/headers.json b/tests/unit/data/endpoints/test-cases/headers.json
new file mode 100644
index 000000000..32186eef6
--- /dev/null
+++ b/tests/unit/data/endpoints/test-cases/headers.json
@@ -0,0 +1,25 @@
+{
+  "version": "1.0",
+  "testCases": [
+    {
+      "documentation": "header set to region",
+      "params": {
+        "Region": "us-east-1"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://us-east-1.amazonaws.com",
+          "headers": {
+            "x-amz-region": [
+              "us-east-1"
+            ],
+            "x-amz-multi": [
+              "*",
+              "us-east-1"
+            ]
+          }
+        }
+      }
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/test-cases/local-region-override.json b/tests/unit/data/endpoints/test-cases/local-region-override.json
new file mode 100644
index 000000000..674105d4b
--- /dev/null
+++ b/tests/unit/data/endpoints/test-cases/local-region-override.json
@@ -0,0 +1,27 @@
+{
+  "version": "1.0",
+  "testCases": [
+    {
+      "documentation": "local region override",
+      "params": {
+        "Region": "local"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "http://localhost:8080"
+        }
+      }
+    },
+    {
+      "documentation": "standard region templated",
+      "params": {
+        "Region": "us-east-2"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://us-east-2.someservice.amazonaws.com"
+        }
+      }
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/test-cases/parse-arn.json b/tests/unit/data/endpoints/test-cases/parse-arn.json
new file mode 100644
index 000000000..3cb90f6f6
--- /dev/null
+++ b/tests/unit/data/endpoints/test-cases/parse-arn.json
@@ -0,0 +1,152 @@
+{
+  "version": "1.0",
+  "testCases": [
+    {
+      "documentation": "arn + region resolution",
+      "params": {
+        "Bucket": "arn:aws:s3:us-east-2:012345678:outpost:op-1234",
+        "Region": "us-east-2"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://op-1234-012345678.us-east-2.amazonaws.com"
+        }
+      }
+    },
+    {
+      "documentation": "arn, unset outpost id",
+      "params": {
+        "Bucket": "arn:aws:s3:us-east-2:012345678:outpost",
+        "Region": "us-east-2"
+      },
+      "expect": {
+        "error": "Invalid ARN: outpostId was not set"
+      }
+    },
+    {
+      "documentation": "arn, empty outpost id (tests that empty strings are handled properly during matching)",
+      "params": {
+        "Bucket": "arn:aws:s3:us-east-2:012345678:outpost::",
+        "Region": "us-east-2"
+      },
+      "expect": {
+        "error": "OutpostId was empty"
+      }
+    },
+    {
+      "documentation": "arn, empty outpost id (tests that ARN parsing considers a trailing colon)",
+      "params": {
+        "Bucket": "arn:aws:s3:us-east-2:012345678:outpost:",
+        "Region": "us-east-2"
+      },
+      "expect": {
+        "error": "OutpostId was empty"
+      }
+    },
+    {
+      "documentation": "valid hostlabel + region resolution",
+      "params": {
+        "Bucket": "mybucket",
+        "Region": "us-east-2"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://mybucket.us-east-2.amazonaws.com"
+        }
+      }
+    },
+    {
+      "documentation": "not a valid hostlabel + region resolution",
+      "params": {
+        "Bucket": "99_a",
+        "Region": "us-east-2"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://us-east-2.amazonaws.com/99_a"
+        }
+      }
+    },
+    {
+      "documentation": "no bucket",
+      "params": {
+        "Region": "us-east-2"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://us-east-2.amazonaws.com"
+        }
+      }
+    },
+    {
+      "documentation": "a string that is not a 6-part ARN",
+      "params": {
+        "TestCaseId": "invalid-arn",
+        "Bucket": "asdf"
+      },
+      "expect": {
+        "error": "Test case passed: `asdf` is not a valid ARN."
+      }
+    },
+    {
+      "documentation": "resource id MUST not be null",
+      "params": {
+        "TestCaseId": "invalid-arn",
+        "Bucket": "arn:aws:s3:us-west-2:123456789012:"
+      },
+      "expect": {
+        "error": "Test case passed: `arn:aws:s3:us-west-2:123456789012:` is not a valid ARN."
+      }
+    },
+    {
+      "documentation": "service MUST not be null",
+      "params": {
+        "TestCaseId": "invalid-arn",
+        "Bucket": "arn:aws::us-west-2:123456789012:resource-id"
+      },
+      "expect": {
+        "error": "Test case passed: `arn:aws::us-west-2:123456789012:resource-id` is not a valid ARN."
+      }
+    },
+    {
+      "documentation": "partition MUST not be null",
+      "params": {
+        "TestCaseId": "invalid-arn",
+        "Bucket": "arn::s3:us-west-2:123456789012:resource-id"
+      },
+      "expect": {
+        "error": "Test case passed: `arn::s3:us-west-2:123456789012:resource-id` is not a valid ARN."
+      }
+    },
+    {
+      "documentation": "region MAY be null",
+      "params": {
+        "TestCaseId": "valid-arn",
+        "Bucket": "arn:aws:s3::123456789012:resource-id"
+      },
+      "expect": {
+        "error": "Test case passed: A valid ARN was parsed: service: `s3`, partition: `aws, region: ``, accountId: `123456789012`, resource: `resource-id`"
+      }
+    },
+    {
+      "documentation": "accountId MAY be null",
+      "params": {
+        "TestCaseId": "valid-arn",
+        "Bucket": "arn:aws:s3:us-east-1::resource-id"
+      },
+      "expect": {
+        "error": "Test case passed: A valid ARN was parsed: service: `s3`, partition: `aws, region: `us-east-1`, accountId: ``, resource: `resource-id`"
+      }
+    },
+    {
+      "documentation": "accountId MAY be non-numeric",
+      "params": {
+        "TestCaseId": "valid-arn",
+        "Bucket": "arn:aws:s3:us-east-1:abcd:resource-id"
+      },
+      "expect": {
+        "error": "Test case passed: A valid ARN was parsed: service: `s3`, partition: `aws, region: `us-east-1`, accountId: `abcd`, resource: `resource-id`"
+      }
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/test-cases/parse-url.json b/tests/unit/data/endpoints/test-cases/parse-url.json
new file mode 100644
index 000000000..1117dad52
--- /dev/null
+++ b/tests/unit/data/endpoints/test-cases/parse-url.json
@@ -0,0 +1,153 @@
+{
+  "version": "1.0",
+  "testCases": [
+    {
+      "documentation": "simple URL parsing",
+      "params": {
+        "Endpoint": "https://authority.com/custom-path"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://https-authority.com.example.com/path-is/custom-path"
+        }
+      }
+    },
+    {
+      "documentation": "empty path no slash",
+      "params": {
+        "Endpoint": "https://authority.com"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://https-authority.com-nopath.example.com"
+        }
+      }
+    },
+    {
+      "documentation": "empty path with slash",
+      "params": {
+        "Endpoint": "https://authority.com/"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://https-authority.com-nopath.example.com"
+        }
+      }
+    },
+    {
+      "documentation": "authority with port",
+      "params": {
+        "Endpoint": "https://authority.com:8000/port"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://authority.com:8000/uri-with-port"
+        }
+      }
+    },
+    {
+      "documentation": "http schemes",
+      "params": {
+        "Endpoint": "http://authority.com:8000/port"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "http://authority.com:8000/uri-with-port"
+        }
+      }
+    },
+    {
+      "documentation": "arbitrary schemes are not supported",
+      "params": {
+        "Endpoint": "acbd://example.com"
+      },
+      "expect": {
+        "error": "endpoint was invalid"
+      }
+    },
+    {
+      "documentation": "host labels are not validated",
+      "params": {
+        "Endpoint": "http://99_ab.com"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://http-99_ab.com-nopath.example.com"
+        }
+      }
+    },
+    {
+      "documentation": "host labels are not validated",
+      "params": {
+        "Endpoint": "http://99_ab-.com"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://http-99_ab-.com-nopath.example.com"
+        }
+      }
+    },
+    {
+      "documentation": "invalid URL",
+      "params": {
+        "Endpoint": "http://abc.com:a/foo"
+      },
+      "expect": {
+        "error": "endpoint was invalid"
+      }
+    },
+    {
+      "documentation": "IP Address",
+      "params": {
+        "Endpoint": "http://192.168.1.1/foo/"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "http://192.168.1.1/foo/is-ip-addr"
+        }
+      }
+    },
+    {
+      "documentation": "IP Address with port",
+      "params": {
+        "Endpoint": "http://192.168.1.1:1234/foo/"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "http://192.168.1.1:1234/foo/is-ip-addr"
+        }
+      }
+    },
+    {
+      "documentation": "IPv6 Address",
+      "params": {
+        "Endpoint": "https://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443/is-ip-addr"
+        }
+      }
+    },
+    {
+      "documentation": "weird DNS name",
+      "params": {
+        "Endpoint": "https://999.999.abc.blah"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://https-999.999.abc.blah-nopath.example.com"
+        }
+      }
+    },
+    {
+      "documentation": "query in resolved endpoint is not supported",
+      "params": {
+        "Endpoint": "https://example.com/path?query1=foo"
+      },
+      "expect": {
+        "error": "endpoint was invalid"
+      }
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/test-cases/partition-fn.json b/tests/unit/data/endpoints/test-cases/partition-fn.json
new file mode 100644
index 000000000..5efc6a708
--- /dev/null
+++ b/tests/unit/data/endpoints/test-cases/partition-fn.json
@@ -0,0 +1,125 @@
+{
+  "version": "1.0",
+  "testCases": [
+    {
+      "documentation": "standard AWS region",
+      "params": {
+        "Region": "us-east-2"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://aws-partition.us-east-2.amazonaws.com",
+          "properties": {
+            "authSchemes": [
+              {
+                "name": "sigv4",
+                "signingName": "serviceName",
+                "signingRegion": "us-east-2"
+              }
+            ],
+            "meta": {
+              "baseSuffix": "amazonaws.com",
+              "dualStackSuffix": "api.aws"
+            }
+          }
+        }
+      }
+    },
+    {
+      "documentation": "AWS region that doesn't match any regexes",
+      "params": {
+        "Region": "mars-global"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://aws-partition.mars-global.amazonaws.com",
+          "properties": {
+            "authSchemes": [
+              {
+                "name": "sigv4",
+                "signingName": "serviceName",
+                "signingRegion": "mars-global"
+              }
+            ],
+            "meta": {
+              "baseSuffix": "amazonaws.com",
+              "dualStackSuffix": "api.aws"
+            }
+          }
+        }
+      }
+    },
+    {
+      "documentation": "AWS region that matches the AWS regex",
+      "params": {
+        "Region": "us-east-10"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://aws-partition.us-east-10.amazonaws.com",
+          "properties": {
+            "authSchemes": [
+              {
+                "name": "sigv4",
+                "signingName": "serviceName",
+                "signingRegion": "us-east-10"
+              }
+            ],
+            "meta": {
+              "baseSuffix": "amazonaws.com",
+              "dualStackSuffix": "api.aws"
+            }
+          }
+        }
+      }
+    },
+    {
+      "documentation": "CN region that matches the AWS regex",
+      "params": {
+        "Region": "cn-north-5"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://aws-cn.cn-north-5.amazonaws.com.cn",
+          "properties": {
+            "authSchemes": [
+              {
+                "name": "sigv4",
+                "signingName": "serviceName",
+                "signingRegion": "cn-north-5"
+              }
+            ],
+            "meta": {
+              "baseSuffix": "amazonaws.com.cn",
+              "dualStackSuffix": "api.amazonwebservices.com.cn"
+            }
+          }
+        }
+      }
+    },
+    {
+      "documentation": "CN region that is in the explicit list",
+      "params": {
+        "Region": "aws-cn-global"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://aws-cn.aws-cn-global.amazonaws.com.cn",
+          "properties": {
+            "authSchemes": [
+              {
+                "name": "sigv4",
+                "signingName": "serviceName",
+                "signingRegion": "aws-cn-global"
+              }
+            ],
+            "meta": {
+              "baseSuffix": "amazonaws.com.cn",
+              "dualStackSuffix": "api.amazonwebservices.com.cn"
+            }
+          }
+        }
+      }
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/test-cases/substring.json b/tests/unit/data/endpoints/test-cases/substring.json
new file mode 100644
index 000000000..c1e2912f9
--- /dev/null
+++ b/tests/unit/data/endpoints/test-cases/substring.json
@@ -0,0 +1,185 @@
+{
+  "version": "1.0",
+  "testCases": [
+    {
+      "documentation": "substring when string is long enough",
+      "params": {
+        "TestCaseId": "1",
+        "Input": "abcdefg"
+      },
+      "expect": {
+        "error": "The value is: `abcd`"
+      }
+    },
+    {
+      "documentation": "substring when string is exactly the right length",
+      "params": {
+        "TestCaseId": "1",
+        "Input": "abcd"
+      },
+      "expect": {
+        "error": "The value is: `abcd`"
+      }
+    },
+    {
+      "documentation": "substring when string is too short",
+      "params": {
+        "TestCaseId": "1",
+        "Input": "abc"
+      },
+      "expect": {
+        "error": "No tests matched"
+      }
+    },
+    {
+      "documentation": "substring when string is too short",
+      "params": {
+        "TestCaseId": "1",
+        "Input": ""
+      },
+      "expect": {
+        "error": "No tests matched"
+      }
+    },
+    {
+      "documentation": "substring on wide characters (ensure that unicode code points are properly counted)",
+      "params": {
+        "TestCaseId": "1",
+        "Input": "\ufdfd"
+      },
+      "expect": {
+        "error": "No tests matched"
+      }
+    },
+    {
+      "documentation": "unicode characters always return `None`",
+      "params": {
+        "TestCaseId": "1",
+        "Input": "abcdef\uD83D\uDC31"
+      },
+      "expect": {
+        "error": "No tests matched"
+      }
+    },
+    {
+      "documentation": "non-ascii cause substring to always return `None`",
+      "params": {
+        "TestCaseId": "1",
+        "Input": "abcdef\u0080"
+      },
+      "expect": {
+        "error": "No tests matched"
+      }
+    },
+    {
+      "documentation": "the full set of ascii is supported, including non-printable characters",
+      "params": {
+        "TestCaseId": "1",
+        "Input": "\u007Fabcdef"
+      },
+      "expect": {
+        "error": "The value is: `\u007Fabc`"
+      }
+    },
+    {
+      "documentation": "substring when string is long enough",
+      "params": {
+        "TestCaseId": "2",
+        "Input": "abcdefg"
+      },
+      "expect": {
+        "error": "The value is: `defg`"
+      }
+    },
+    {
+      "documentation": "substring when string is exactly the right length",
+      "params": {
+        "TestCaseId": "2",
+        "Input": "defg"
+      },
+      "expect": {
+        "error": "The value is: `defg`"
+      }
+    },
+    {
+      "documentation": "substring when string is too short",
+      "params": {
+        "TestCaseId": "2",
+        "Input": "abc"
+      },
+      "expect": {
+        "error": "No tests matched"
+      }
+    },
+    {
+      "documentation": "substring when string is too short",
+      "params": {
+        "TestCaseId": "2",
+        "Input": ""
+      },
+      "expect": {
+        "error": "No tests matched"
+      }
+    },
+    {
+      "documentation": "substring on wide characters (ensure that unicode code points are properly counted)",
+      "params": {
+        "TestCaseId": "2",
+        "Input": "\ufdfd"
+      },
+      "expect": {
+        "error": "No tests matched"
+      }
+    },
+    {
+      "documentation": "substring when string is longer",
+      "params": {
+        "TestCaseId": "3",
+        "Input": "defg"
+      },
+      "expect": {
+        "error": "The value is: `ef`"
+      }
+    },
+    {
+      "documentation": "substring when string is exact length",
+      "params": {
+        "TestCaseId": "3",
+        "Input": "def"
+      },
+      "expect": {
+        "error": "The value is: `ef`"
+      }
+    },
+    {
+      "documentation": "substring when string is too short",
+      "params": {
+        "TestCaseId": "3",
+        "Input": "ab"
+      },
+      "expect": {
+        "error": "No tests matched"
+      }
+    },
+    {
+      "documentation": "substring when string is too short",
+      "params": {
+        "TestCaseId": "3",
+        "Input": ""
+      },
+      "expect": {
+        "error": "No tests matched"
+      }
+    },
+    {
+      "documentation": "substring on wide characters (ensure that unicode code points are properly counted)",
+      "params": {
+        "TestCaseId": "3",
+        "Input": "\ufdfd"
+      },
+      "expect": {
+        "error": "No tests matched"
+      }
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/test-cases/uri-encode.json b/tests/unit/data/endpoints/test-cases/uri-encode.json
new file mode 100644
index 000000000..6da5f21b5
--- /dev/null
+++ b/tests/unit/data/endpoints/test-cases/uri-encode.json
@@ -0,0 +1,75 @@
+{
+  "version": "1.0",
+  "testCases": [
+    {
+      "documentation": "uriEncode when the string has nothing to encode returns the input",
+      "params": {
+        "TestCaseId": "1",
+        "Input": "abcdefg"
+      },
+      "expect": {
+        "error": "The value is: `abcdefg`"
+      }
+    },
+    {
+      "documentation": "uriEncode with single character to encode encodes only that character",
+      "params": {
+        "TestCaseId": "1",
+        "Input": "abc:defg"
+      },
+      "expect": {
+        "error": "The value is: `abc%3Adefg`"
+      }
+    },
+    {
+      "documentation": "uriEncode with all ASCII characters to encode encodes all characters",
+      "params": {
+        "TestCaseId": "1",
+        "Input": "/:,?#[]{}|@! $&'()*+;=%<>\"^`\\"
+      },
+      "expect": {
+        "error": "The value is: `%2F%3A%2C%3F%23%5B%5D%7B%7D%7C%40%21%20%24%26%27%28%29%2A%2B%3B%3D%25%3C%3E%22%5E%60%5C`"
+      }
+    },
+    {
+      "documentation": "uriEncode with ASCII characters that should not be encoded returns the input",
+      "params": {
+        "TestCaseId": "1",
+        "Input": "0123456789.underscore_dash-Tilda~"
+      },
+      "expect": {
+        "error": "The value is: `0123456789.underscore_dash-Tilda~`"
+      }
+    },
+    {
+      "documentation": "uriEncode encodes unicode characters",
+      "params": {
+        "TestCaseId": "1",
+        "Input": "\ud83d\ude39"
+      },
+      "expect": {
+        "error": "The value is: `%F0%9F%98%B9`"
+      }
+    },
+    {
+      "documentation": "uriEncode on all printable ASCII characters",
+      "params": {
+        "TestCaseId": "1",
+        "Input": " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
+      },
+      "expect": {
+        "error": "The value is: `%20%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F0123456789%3A%3B%3C%3D%3E%3F%40ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~`"
+      }
+    },
+    {
+      "documentation": "uriEncode on an empty string",
+      "params": {
+        "TestCaseId": "1",
+        "Input": ""
+      },
+      "expect": {
+        "error": "The value is: ``"
+      }
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/test-cases/valid-hostlabel.json b/tests/unit/data/endpoints/test-cases/valid-hostlabel.json
new file mode 100644
index 000000000..70f148d4b
--- /dev/null
+++ b/tests/unit/data/endpoints/test-cases/valid-hostlabel.json
@@ -0,0 +1,47 @@
+{
+  "version": "1.0",
+  "testCases": [
+    {
+      "documentation": "standard region is a valid hostlabel",
+      "params": {
+        "Region": "us-east-1"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://us-east-1.amazonaws.com"
+        }
+      }
+    },
+    {
+      "documentation": "starting with a number is a valid hostlabel",
+      "params": {
+        "Region": "3aws4"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://3aws4.amazonaws.com"
+        }
+      }
+    },
+    {
+      "documentation": "when there are dots, only match if subdomains are allowed",
+      "params": {
+        "Region": "part1.part2"
+      },
+      "expect": {
+        "endpoint": {
+          "url": "https://part1.part2-subdomains.amazonaws.com"
+        }
+      }
+    },
+    {
+      "documentation": "a space is never a valid hostlabel",
+      "params": {
+        "Region": "part1 part2"
+      },
+      "expect": {
+        "error": "Invalid hostlabel"
+      }
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/valid-rules/aws-region.json b/tests/unit/data/endpoints/valid-rules/aws-region.json
new file mode 100644
index 000000000..2bb8e7fa6
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/aws-region.json
@@ -0,0 +1,44 @@
+{
+  "parameters": {
+    "Region": {
+      "type": "string",
+      "builtIn": "AWS::Region",
+      "documentation": "The region to dispatch this request, eg. `us-east-1`."
+    }
+  },
+  "rules": [
+    {
+      "documentation": "Template the region into the URI when region is set",
+      "conditions": [
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "Region"
+            }
+          ]
+        }
+      ],
+      "endpoint": {
+        "url": "https://{Region}.amazonaws.com",
+        "properties": {
+          "authSchemes": [
+            {
+              "name": "sigv4",
+              "signingName": "serviceName",
+              "signingRegion": "{Region}"
+            }
+          ]
+        }
+      },
+      "type": "endpoint"
+    },
+    {
+      "documentation": "fallback when region is unset",
+      "conditions": [],
+      "error": "Region must be set to resolve a valid endpoint",
+      "type": "error"
+    }
+  ],
+  "version": "1.3"
+}
diff --git a/tests/unit/data/endpoints/valid-rules/default-values.json b/tests/unit/data/endpoints/valid-rules/default-values.json
new file mode 100644
index 000000000..42c95b43c
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/default-values.json
@@ -0,0 +1,44 @@
+{
+  "parameters": {
+    "Region": {
+      "type": "string",
+      "builtIn": "AWS::Region",
+      "documentation": "The region to dispatch this request, eg. `us-east-1`.",
+      "default": "us-west-5",
+      "required": true
+    },
+    "UseFips": {
+      "type": "boolean",
+      "builtIn": "AWS::UseFIPS",
+      "default": true,
+      "required": true
+    }
+  },
+  "rules": [
+    {
+      "documentation": "Template the region into the URI when FIPS is enabled",
+      "conditions": [
+        {
+          "fn": "booleanEquals",
+          "argv": [
+            {
+              "ref": "UseFips"
+            },
+            true
+          ]
+        }
+      ],
+      "endpoint": {
+        "url": "https://fips.{Region}.amazonaws.com"
+      },
+      "type": "endpoint"
+    },
+    {
+      "documentation": "error when fips is disabled",
+      "conditions": [],
+      "error": "UseFips = false",
+      "type": "error"
+    }
+  ],
+  "version": "1.3"
+}
diff --git a/tests/unit/data/endpoints/valid-rules/deprecated-param.json b/tests/unit/data/endpoints/valid-rules/deprecated-param.json
new file mode 100644
index 000000000..52b312a00
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/deprecated-param.json
@@ -0,0 +1,41 @@
+{
+  "parameters": {
+    "Region": {
+      "type": "string",
+      "builtIn": "AWS::Region",
+      "required": false,
+      "deprecated": {
+        "message": "use blahdeblah region instead"
+      }
+    }
+  },
+  "rules": [
+    {
+      "documentation": "base rule",
+      "conditions": [
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "Region"
+            }
+          ]
+        }
+      ],
+      "endpoint": {
+        "url": "https://{Region}.amazonaws.com",
+        "properties": {
+          "authSchemes": [
+            {
+              "name": "sigv4",
+              "signingName": "serviceName",
+              "signingRegion": "{Region}"
+            }
+          ]
+        }
+      },
+      "type": "endpoint"
+    }
+  ],
+  "version": "1.3"
+}
diff --git a/tests/unit/data/endpoints/valid-rules/eventbridge.json b/tests/unit/data/endpoints/valid-rules/eventbridge.json
new file mode 100644
index 000000000..81e684437
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/eventbridge.json
@@ -0,0 +1,323 @@
+{
+  "version": "1.3",
+  "parameters": {
+    "region": {
+      "type": "string",
+      "builtIn": "AWS::Region",
+      "required": true
+    },
+    "useDualStackEndpoint": {
+      "type": "boolean",
+      "builtIn": "AWS::UseDualStack"
+    },
+    "useFIPSEndpoint": {
+      "type": "boolean",
+      "builtIn": "AWS::UseFIPS"
+    },
+    "endpointId": {
+      "type": "string"
+    }
+  },
+  "rules": [
+    {
+      "conditions": [
+        {
+          "fn": "aws.partition",
+          "argv": [
+            {
+              "ref": "region"
+            }
+          ],
+          "assign": "partitionResult"
+        }
+      ],
+      "rules": [
+        {
+          "conditions": [
+            {
+              "fn": "isSet",
+              "argv": [
+                {
+                  "ref": "endpointId"
+                }
+              ]
+            }
+          ],
+          "rules": [
+            {
+              "conditions": [
+                {
+                  "fn": "isSet",
+                  "argv": [
+                    {
+                      "ref": "useFIPSEndpoint"
+                    }
+                  ]
+                },
+                {
+                  "fn": "booleanEquals",
+                  "argv": [
+                    {
+                      "ref": "useFIPSEndpoint"
+                    },
+                    true
+                  ]
+                }
+              ],
+              "error": "FIPS endpoints not supported with multi-region endpoints",
+              "type": "error"
+            },
+            {
+              "conditions": [
+                {
+                  "fn": "not",
+                  "argv": [
+                    {
+                      "fn": "isSet",
+                      "argv": [
+                        {
+                          "ref": "useFIPSEndpoint"
+                        }
+                      ]
+                    }
+                  ]
+                },
+                {
+                  "fn": "isSet",
+                  "argv": [
+                    {
+                      "ref": "useDualStackEndpoint"
+                    }
+                  ]
+                },
+                {
+                  "fn": "booleanEquals",
+                  "argv": [
+                    {
+                      "ref": "useDualStackEndpoint"
+                    },
+                    true
+                  ]
+                }
+              ],
+              "endpoint": {
+                "url": "https://{endpointId}.endpoint.events.{partitionResult#dualStackDnsSuffix}",
+                "properties": {
+                  "authSchemes": [
+                    {
+                      "name": "sigv4a",
+                      "signingName": "events",
+                      "signingRegionSet": [
+                        "*"
+                      ]
+                    }
+                  ]
+                }
+              },
+              "type": "endpoint"
+            },
+            {
+              "conditions": [],
+              "endpoint": {
+                "url": "https://{endpointId}.endpoint.events.{partitionResult#dnsSuffix}",
+                "properties": {
+                  "authSchemes": [
+                    {
+                      "name": "sigv4a",
+                      "signingName": "events",
+                      "signingRegionSet": [
+                        "*"
+                      ]
+                    }
+                  ]
+                }
+              },
+              "type": "endpoint"
+            }
+          ],
+          "type": "tree"
+        },
+        {
+          "conditions": [
+            {
+              "fn": "isValidHostLabel",
+              "argv": [
+                {
+                  "ref": "region"
+                },
+                false
+              ]
+            }
+          ],
+          "rules": [
+            {
+              "conditions": [
+                {
+                  "fn": "isSet",
+                  "argv": [
+                    {
+                      "ref": "useFIPSEndpoint"
+                    }
+                  ]
+                },
+                {
+                  "fn": "booleanEquals",
+                  "argv": [
+                    {
+                      "ref": "useFIPSEndpoint"
+                    },
+                    true
+                  ]
+                },
+                {
+                  "fn": "not",
+                  "argv": [
+                    {
+                      "fn": "isSet",
+                      "argv": [
+                        {
+                          "ref": "useDualStackEndpoint"
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ],
+              "endpoint": {
+                "url": "https://events-fips.{region}.{partitionResult#dnsSuffix}",
+                "properties": {
+                  "authSchemes": [
+                    {
+                      "name": "sigv4a",
+                      "signingName": "events",
+                      "signingRegionSet": [
+                        "*"
+                      ]
+                    }
+                  ]
+                }
+              },
+              "type": "endpoint"
+            },
+            {
+              "conditions": [
+                {
+                  "fn": "isSet",
+                  "argv": [
+                    {
+                      "ref": "useDualStackEndpoint"
+                    }
+                  ]
+                },
+                {
+                  "fn": "booleanEquals",
+                  "argv": [
+                    {
+                      "ref": "useDualStackEndpoint"
+                    },
+                    true
+                  ]
+                },
+                {
+                  "fn": "not",
+                  "argv": [
+                    {
+                      "fn": "isSet",
+                      "argv": [
+                        {
+                          "ref": "useFIPSEndpoint"
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ],
+              "endpoint": {
+                "url": "https://events.{region}.{partitionResult#dualStackDnsSuffix}",
+                "properties": {
+                  "authSchemes": [
+                    {
+                      "name": "sigv4a",
+                      "signingName": "events",
+                      "signingRegionSet": [
+                        "*"
+                      ]
+                    }
+                  ]
+                }
+              },
+              "type": "endpoint"
+            },
+            {
+              "conditions": [
+                {
+                  "fn": "isSet",
+                  "argv": [
+                    {
+                      "ref": "useDualStackEndpoint"
+                    }
+                  ]
+                },
+                {
+                  "fn": "isSet",
+                  "argv": [
+                    {
+                      "ref": "useFIPSEndpoint"
+                    }
+                  ]
+                },
+                {
+                  "fn": "booleanEquals",
+                  "argv": [
+                    {
+                      "ref": "useDualStackEndpoint"
+                    },
+                    true
+                  ]
+                },
+                {
+                  "fn": "booleanEquals",
+                  "argv": [
+                    {
+                      "ref": "useFIPSEndpoint"
+                    },
+                    true
+                  ]
+                }
+              ],
+              "endpoint": {
+                "url": "https://events-fips.{region}.{partitionResult#dualStackDnsSuffix}",
+                "properties": {
+                  "authSchemes": [
+                    {
+                      "name": "sigv4a",
+                      "signingName": "events",
+                      "signingRegionSet": [
+                        "*"
+                      ]
+                    }
+                  ]
+                }
+              },
+              "type": "endpoint"
+            },
+            {
+              "conditions": [],
+              "endpoint": {
+                "url": "https://events.{region}.{partitionResult#dnsSuffix}"
+              },
+              "type": "endpoint"
+            }
+          ],
+          "type": "tree"
+        },
+        {
+          "conditions": [],
+          "error": "{region} is not a valid HTTP host-label",
+          "type": "error"
+        }
+      ],
+      "type": "tree"
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/valid-rules/fns.json b/tests/unit/data/endpoints/valid-rules/fns.json
new file mode 100644
index 000000000..526b101c7
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/fns.json
@@ -0,0 +1,128 @@
+{
+  "documentation": "functions in more places",
+  "parameters": {
+    "Uri": {
+      "type": "string",
+      "documentation": "A URI to use"
+    },
+    "Arn": {
+      "type": "string",
+      "documentation": "an ARN to extract fields from"
+    },
+    "CustomError": {
+      "type": "string",
+      "documentation": "when set, a custom error message"
+    }
+  },
+  "rules": [
+    {
+      "documentation": "when URI is set, use it directly",
+      "conditions": [
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "Uri"
+            }
+          ]
+        },
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "Arn"
+            }
+          ]
+        },
+        {
+          "fn": "aws.parseArn",
+          "argv": [
+            {
+              "ref": "Arn"
+            }
+          ],
+          "assign": "parsedArn"
+        }
+      ],
+      "endpoint": {
+        "url": {
+          "ref": "Uri"
+        },
+        "headers": {
+          "x-uri": [
+            {
+              "ref": "Uri"
+            }
+          ],
+          "x-arn-region": [
+            {
+              "fn": "getAttr",
+              "argv": [
+                {
+                  "ref": "parsedArn"
+                },
+                "region"
+              ]
+            }
+          ]
+        }
+      },
+      "type": "endpoint"
+    },
+    {
+      "documentation": "A custom error",
+      "conditions": [
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "CustomError"
+            }
+          ]
+        }
+      ],
+      "type": "error",
+      "error": {
+        "ref": "CustomError"
+      }
+    },
+    {
+      "type": "error",
+      "conditions": [
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "Arn"
+            }
+          ]
+        },
+        {
+          "fn": "aws.parseArn",
+          "argv": [
+            {
+              "ref": "Arn"
+            }
+          ],
+          "assign": "parsedArn"
+        }
+      ],
+      "error": {
+        "fn": "getAttr",
+        "argv": [
+          {
+            "ref": "parsedArn"
+          },
+          "partition"
+        ]
+      }
+    },
+    {
+      "documentation": "fallback when nothing is set",
+      "conditions": [],
+      "error": "No fields were set",
+      "type": "error"
+    }
+  ],
+  "version": "1.3"
+}
diff --git a/tests/unit/data/endpoints/valid-rules/get-attr-type-inference.json b/tests/unit/data/endpoints/valid-rules/get-attr-type-inference.json
new file mode 100644
index 000000000..cb631fc17
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/get-attr-type-inference.json
@@ -0,0 +1,46 @@
+{
+  "version": "1.3",
+  "parameters": {
+    "Bucket": {
+      "type": "string"
+    }
+  },
+  "rules": [
+    {
+      "documentation": "bucket is set, handle bucket specific endpoints",
+      "conditions": [
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "Bucket"
+            }
+          ]
+        },
+        {
+          "fn": "aws.parseArn",
+          "argv": [
+            {
+              "ref": "Bucket"
+            }
+          ],
+          "assign": "bucketArn"
+        },
+        {
+          "fn": "getAttr",
+          "argv": [
+            {
+              "ref": "bucketArn"
+            },
+            "resourceId[2]"
+          ],
+          "assign": "outpostId"
+        }
+      ],
+      "endpoint": {
+        "url": "https://{bucketArn#accountId}.{outpostId}.{bucketArn#region}"
+      },
+      "type": "endpoint"
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/valid-rules/headers.json b/tests/unit/data/endpoints/valid-rules/headers.json
new file mode 100644
index 000000000..6145f852c
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/headers.json
@@ -0,0 +1,44 @@
+{
+  "parameters": {
+    "Region": {
+      "type": "string",
+      "builtIn": "AWS::Region",
+      "documentation": "The region to dispatch this request, eg. `us-east-1`."
+    }
+  },
+  "rules": [
+    {
+      "documentation": "Template the region into the URI when region is set",
+      "conditions": [
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "Region"
+            }
+          ]
+        }
+      ],
+      "endpoint": {
+        "url": "https://{Region}.amazonaws.com",
+        "headers": {
+          "x-amz-region": [
+            "{Region}"
+          ],
+          "x-amz-multi": [
+            "*",
+            "{Region}"
+          ]
+        }
+      },
+      "type": "endpoint"
+    },
+    {
+      "documentation": "fallback when region is unset",
+      "conditions": [],
+      "error": "Region must be set to resolve a valid endpoint",
+      "type": "error"
+    }
+  ],
+  "version": "1.3"
+}
diff --git a/tests/unit/data/endpoints/valid-rules/local-region-override.json b/tests/unit/data/endpoints/valid-rules/local-region-override.json
new file mode 100644
index 000000000..2100b4ec1
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/local-region-override.json
@@ -0,0 +1,36 @@
+{
+  "parameters": {
+    "Region": {
+      "type": "string",
+      "builtIn": "AWS::Region",
+      "required": true
+    }
+  },
+  "rules": [
+    {
+      "documentation": "override rule for the local pseduo region",
+      "conditions": [
+        {
+          "fn": "stringEquals",
+          "argv": [
+            "local",
+            "{Region}"
+          ]
+        }
+      ],
+      "endpoint": {
+        "url": "http://localhost:8080"
+      },
+      "type": "endpoint"
+    },
+    {
+      "documentation": "base rule",
+      "conditions": [],
+      "endpoint": {
+        "url": "https://{Region}.someservice.amazonaws.com"
+      },
+      "type": "endpoint"
+    }
+  ],
+  "version": "1.3"
+}
diff --git a/tests/unit/data/endpoints/valid-rules/minimal-ruleset.json b/tests/unit/data/endpoints/valid-rules/minimal-ruleset.json
new file mode 100644
index 000000000..8c629f842
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/minimal-ruleset.json
@@ -0,0 +1,29 @@
+{
+  "parameters": {
+    "Region": {
+      "type": "string",
+      "builtIn": "AWS::Region",
+      "required": true
+    }
+  },
+  "rules": [
+    {
+      "documentation": "base rule",
+      "conditions": [],
+      "endpoint": {
+        "url": "https://{Region}.amazonaws.com",
+        "properties": {
+          "authSchemes": [
+            {
+              "name": "sigv4",
+              "signingName": "serviceName",
+              "signingRegion": "{Region}"
+            }
+          ]
+        }
+      },
+      "type": "endpoint"
+    }
+  ],
+  "version": "1.3"
+}
diff --git a/tests/unit/data/endpoints/valid-rules/parse-arn.json b/tests/unit/data/endpoints/valid-rules/parse-arn.json
new file mode 100644
index 000000000..27664e64e
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/parse-arn.json
@@ -0,0 +1,251 @@
+{
+  "version": "1.3",
+  "parameters": {
+    "Region": {
+      "type": "string",
+      "builtIn": "AWS::Region"
+    },
+    "Bucket": {
+      "type": "string"
+    },
+    "TestCaseId": {
+      "type": "string"
+    }
+  },
+  "rules": [
+    {
+      "documentation": "tests of invalid arns",
+      "conditions": [
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "TestCaseId"
+            }
+          ]
+        },
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "Bucket"
+            }
+          ]
+        },
+        {
+          "fn": "stringEquals",
+          "argv": [
+            "{TestCaseId}",
+            "invalid-arn"
+          ]
+        }
+      ],
+      "type": "tree",
+      "rules": [
+        {
+          "conditions": [
+            {
+              "fn": "aws.parseArn",
+              "argv": ["{Bucket}"]
+            }
+          ],
+          "type": "error",
+          "error": "A valid ARN was parsed but `{Bucket}` is not a valid ARN"
+        },
+        {
+          "conditions": [],
+          "type": "error",
+          "error": "Test case passed: `{Bucket}` is not a valid ARN."
+        }
+      ]
+    },
+    {
+      "documentation": "tests of valid arns",
+      "conditions": [
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "TestCaseId"
+            }
+          ]
+        },
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "Bucket"
+            }
+          ]
+        },
+        {
+          "fn": "stringEquals",
+          "argv": [
+            "{TestCaseId}",
+            "valid-arn"
+          ]
+        }
+      ],
+      "type": "tree",
+      "rules": [
+        {
+          "conditions": [
+            {
+              "fn": "aws.parseArn",
+              "argv": ["{Bucket}"],
+              "assign": "arn"
+            },
+            {
+              "fn": "getAttr",
+              "argv": [{"ref": "arn"}, "resourceId[0]"],
+              "assign": "resource"
+            }
+          ],
+          "type": "error",
+          "error": "Test case passed: A valid ARN was parsed: service: `{arn#service}`, partition: `{arn#partition}, region: `{arn#region}`, accountId: `{arn#accountId}`, resource: `{resource}`"
+        },
+        {
+          "conditions": [],
+          "type": "error",
+          "error": "Test case failed: `{Bucket}` is a valid ARN but parseArn failed to parse it."
+        }
+      ]
+    },
+    {
+      "documentation": "region is set",
+      "conditions": [
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "Region"
+            }
+          ]
+        },
+        {
+          "fn": "aws.partition",
+          "argv": [
+            "{Region}"
+          ],
+          "assign": "partitionResult"
+        }
+      ],
+      "rules": [
+        {
+          "documentation": "bucket is set, handle bucket specific endpoints",
+          "conditions": [
+            {
+              "fn": "isSet",
+              "argv": [
+                {
+                  "ref": "Bucket"
+                }
+              ]
+            }
+          ],
+          "rules": [
+            {
+              "documentation": "bucket is set and is an arn",
+              "conditions": [
+                {
+                  "fn": "aws.parseArn",
+                  "argv": [
+                    {
+                      "ref": "Bucket"
+                    }
+                  ],
+                  "assign": "bucketArn"
+                }
+              ],
+              "rules": [
+                {
+                  "conditions": [
+                    {
+                      "fn": "getAttr",
+                      "argv": [
+                        {
+                          "ref": "bucketArn"
+                        },
+                        "resourceId[1]"
+                      ],
+                      "assign": "outpostId"
+                    }
+                  ],
+                  "rules": [
+                    {
+                      "conditions": [
+                        {
+                          "fn": "stringEquals",
+                          "argv": [
+                            "{outpostId}",
+                            ""
+                          ]
+                        }
+                      ],
+                      "error": "OutpostId was empty",
+                      "type": "error"
+                    },
+                    {
+                      "conditions": [],
+                      "endpoint": {
+                        "url": "https://{outpostId}-{bucketArn#accountId}.{bucketArn#region}.{partitionResult#dnsSuffix}"
+                      },
+                      "type": "endpoint"
+                    }
+                  ],
+                  "type": "tree"
+                },
+                {
+                  "conditions": [],
+                  "error": "Invalid ARN: outpostId was not set",
+                  "type": "error"
+                }
+              ],
+              "type": "tree"
+            },
+            {
+              "documentation": "bucket can be used as a host label",
+              "conditions": [
+                {
+                  "fn": "isValidHostLabel",
+                  "argv": [
+                    "{Bucket}",
+                    false
+                  ]
+                }
+              ],
+              "endpoint": {
+                "url": "https://{Bucket}.{Region}.amazonaws.com"
+              },
+              "type": "endpoint"
+            },
+            {
+              "conditions": [],
+              "documentation": "fallback: use bucket in the path",
+              "endpoint": {
+                "url": "https://{Region}.amazonaws.com/{Bucket}"
+              },
+              "type": "endpoint"
+            }
+          ],
+          "type": "tree"
+        },
+        {
+          "documentation": "region is set, bucket is not",
+          "conditions": [],
+          "endpoint": {
+            "url": "https://{Region}.{partitionResult#dnsSuffix}"
+          },
+          "type": "endpoint"
+        }
+      ],
+      "type": "tree"
+    },
+    {
+      "documentation": "fallback when region is unset",
+      "conditions": [],
+      "error": "Region must be set to resolve a valid endpoint",
+      "type": "error"
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/valid-rules/parse-url.json b/tests/unit/data/endpoints/valid-rules/parse-url.json
new file mode 100644
index 000000000..7c6dc985d
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/parse-url.json
@@ -0,0 +1,102 @@
+{
+  "version": "1.3",
+  "parameters": {
+    "Region": {
+      "type": "string",
+      "builtIn": "AWS::Region"
+    },
+    "Endpoint": {
+      "type": "string"
+    }
+  },
+  "rules": [
+    {
+      "documentation": "endpoint is set and is a valid URL",
+      "conditions": [
+        {
+          "fn": "isSet",
+          "argv": [
+            {
+              "ref": "Endpoint"
+            }
+          ]
+        },
+        {
+          "fn": "parseURL",
+          "argv": [
+            "{Endpoint}"
+          ],
+          "assign": "url"
+        }
+      ],
+      "rules": [
+        {
+          "conditions": [
+            {
+              "fn": "booleanEquals",
+              "argv": [
+                {
+                  "fn": "getAttr",
+                  "argv": [
+                    {
+                      "ref": "url"
+                    },
+                    "isIp"
+                  ]
+                },
+                true
+              ]
+            }
+          ],
+          "endpoint": {
+            "url": "{url#scheme}://{url#authority}{url#normalizedPath}is-ip-addr"
+          },
+          "type": "endpoint"
+        },
+        {
+          "conditions": [
+            {
+              "fn": "stringEquals",
+              "argv": [
+                "{url#path}",
+                "/port"
+              ]
+            }
+          ],
+          "endpoint": {
+            "url": "{url#scheme}://{url#authority}/uri-with-port"
+          },
+          "type": "endpoint"
+        },
+        {
+          "conditions": [
+            {
+              "fn": "stringEquals",
+              "argv": [
+                "{url#normalizedPath}",
+                "/"
+              ]
+            }
+          ],
+          "endpoint": {
+            "url": "https://{url#scheme}-{url#authority}-nopath.example.com"
+          },
+          "type": "endpoint"
+        },
+        {
+          "conditions": [],
+          "endpoint": {
+            "url": "https://{url#scheme}-{url#authority}.example.com/path-is{url#path}"
+          },
+          "type": "endpoint"
+        }
+      ],
+      "type": "tree"
+    },
+    {
+      "error": "endpoint was invalid",
+      "conditions": [],
+      "type": "error"
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/valid-rules/partition-fn.json b/tests/unit/data/endpoints/valid-rules/partition-fn.json
new file mode 100644
index 000000000..79f94e53a
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/partition-fn.json
@@ -0,0 +1,101 @@
+{
+  "parameters": {
+    "Region": {
+      "type": "string",
+      "builtIn": "AWS::Region",
+      "required": true
+    },
+    "PropertyOne": {
+      "type": "boolean"
+    },
+    "PropertyTwo": {
+      "type": "string"
+    },
+    "PropertyThree": {
+      "type": "boolean"
+    }
+  },
+  "rules": [
+    {
+      "documentation": "base rule",
+      "conditions": [
+        {
+          "fn": "aws.partition",
+          "argv": [
+            {
+              "ref": "Region"
+            }
+          ],
+          "assign": "PartResult"
+        }
+      ],
+      "rules": [
+        {
+          "documentation": "the AWS partition",
+          "conditions": [
+            {
+              "fn": "stringEquals",
+              "argv": [
+                "aws",
+                {
+                  "fn": "getAttr",
+                  "argv": [
+                    {
+                      "ref": "PartResult"
+                    },
+                    "name"
+                  ]
+                }
+              ]
+            }
+          ],
+          "endpoint": {
+            "url": "https://aws-partition.{Region}.{PartResult#dnsSuffix}",
+            "properties": {
+              "authSchemes": [
+                {
+                  "name": "sigv4",
+                  "signingName": "serviceName",
+                  "signingRegion": "{Region}"
+                }
+              ],
+              "meta": {
+                "baseSuffix": "{PartResult#dnsSuffix}",
+                "dualStackSuffix": "{PartResult#dualStackDnsSuffix}"
+              }
+            }
+          },
+          "type": "endpoint"
+        },
+        {
+          "documentation": "the other partitions",
+          "conditions": [],
+          "endpoint": {
+            "url": "https://{PartResult#name}.{Region}.{PartResult#dnsSuffix}",
+            "properties": {
+              "authSchemes": [
+                {
+                  "name": "sigv4",
+                  "signingName": "serviceName",
+                  "signingRegion": "{Region}"
+                }
+              ],
+              "meta": {
+                "baseSuffix": "{PartResult#dnsSuffix}",
+                "dualStackSuffix": "{PartResult#dualStackDnsSuffix}"
+              }
+            }
+          },
+          "type": "endpoint"
+        },
+        {
+          "conditions": [],
+          "error": "no rules matched",
+          "type": "error"
+        }
+      ],
+      "type": "tree"
+    }
+  ],
+  "version": "1.3"
+}
diff --git a/tests/unit/data/endpoints/valid-rules/substring.json b/tests/unit/data/endpoints/valid-rules/substring.json
new file mode 100644
index 000000000..4342d0501
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/substring.json
@@ -0,0 +1,95 @@
+{
+  "parameters": {
+    "TestCaseId": {
+      "type": "string",
+      "required": true,
+      "documentation": "Test case id used to select the test case to use"
+    },
+    "Input": {
+      "type": "string",
+      "required": true,
+      "documentation": "the input used to test substring"
+    }
+  },
+  "rules": [
+    {
+      "documentation": "Substring from beginning of input",
+      "conditions": [
+        {
+          "fn": "stringEquals",
+          "argv": [
+            "{TestCaseId}",
+            "1"
+          ]
+        },
+        {
+          "fn": "substring",
+          "argv": [
+            "{Input}",
+            0,
+            4,
+            false
+          ],
+          "assign": "output"
+        }
+      ],
+      "error": "The value is: `{output}`",
+      "type": "error"
+    },
+    {
+      "documentation": "Substring from end of input",
+      "conditions": [
+        {
+          "fn": "stringEquals",
+          "argv": [
+            "{TestCaseId}",
+            "2"
+          ]
+        },
+        {
+          "fn": "substring",
+          "argv": [
+            "{Input}",
+            0,
+            4,
+            true
+          ],
+          "assign": "output"
+        }
+      ],
+      "error": "The value is: `{output}`",
+      "type": "error"
+    },
+    {
+      "documentation": "Substring the middle of the string",
+      "conditions": [
+        {
+          "fn": "stringEquals",
+          "argv": [
+            "{TestCaseId}",
+            "3"
+          ]
+        },
+        {
+          "fn": "substring",
+          "argv": [
+            "{Input}",
+            1,
+            3,
+            false
+          ],
+          "assign": "output"
+        }
+      ],
+      "error": "The value is: `{output}`",
+      "type": "error"
+    },
+    {
+      "documentation": "fallback when no tests match",
+      "conditions": [],
+      "error": "No tests matched",
+      "type": "error"
+    }
+  ],
+  "version": "1.3"
+}
diff --git a/tests/unit/data/endpoints/valid-rules/uri-encode.json b/tests/unit/data/endpoints/valid-rules/uri-encode.json
new file mode 100644
index 000000000..7c2f62fac
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/uri-encode.json
@@ -0,0 +1,44 @@
+{
+  "version": "1.3",
+  "parameters": {
+    "TestCaseId": {
+      "type": "string",
+      "required": true,
+      "documentation": "Test case id used to select the test case to use"
+    },
+    "Input": {
+      "type": "string",
+      "required": true,
+      "documentation": "the input used to test uriEncode"
+    }
+  },
+  "rules": [
+    {
+      "documentation": "uriEncode on input",
+      "conditions": [
+        {
+          "fn": "stringEquals",
+          "argv": [
+            "{TestCaseId}",
+            "1"
+          ]
+        },
+        {
+          "fn": "uriEncode",
+          "argv": [
+            "{Input}"
+          ],
+          "assign": "output"
+        }
+      ],
+      "error": "The value is: `{output}`",
+      "type": "error"
+    },
+    {
+      "documentation": "fallback when no tests match",
+      "conditions": [],
+      "error": "No tests matched",
+      "type": "error"
+    }
+  ]
+}
diff --git a/tests/unit/data/endpoints/valid-rules/valid-hostlabel.json b/tests/unit/data/endpoints/valid-rules/valid-hostlabel.json
new file mode 100644
index 000000000..d62e25af3
--- /dev/null
+++ b/tests/unit/data/endpoints/valid-rules/valid-hostlabel.json
@@ -0,0 +1,55 @@
+{
+  "parameters": {
+    "Region": {
+      "type": "string",
+      "builtIn": "AWS::Region",
+      "required": true,
+      "documentation": "The region to dispatch this request, eg. `us-east-1`."
+    }
+  },
+  "rules": [
+    {
+      "documentation": "Template the region into the URI when region is set",
+      "conditions": [
+        {
+          "fn": "isValidHostLabel",
+          "argv": [
+            {
+              "ref": "Region"
+            },
+            false
+          ]
+        }
+      ],
+      "endpoint": {
+        "url": "https://{Region}.amazonaws.com"
+      },
+      "type": "endpoint"
+    },
+    {
+      "documentation": "Template the region into the URI when region is set",
+      "conditions": [
+        {
+          "fn": "isValidHostLabel",
+          "argv": [
+            {
+              "ref": "Region"
+            },
+            true
+          ]
+        }
+      ],
+      "endpoint": {
+        "url": "https://{Region}-subdomains.amazonaws.com"
+      },
+      "type": "endpoint"
+    },
+    {
+      "documentation": "Region was not a valid host label",
+      "conditions": [],
+      "error": "Invalid hostlabel",
+      "type": "error"
+    }
+  ],
+  "version": "1.3"
+}
diff --git a/tests/unit/test_endpoints_v2.py b/tests/unit/test_endpoints_v2.py
new file mode 100644
index 000000000..a71e60082
--- /dev/null
+++ b/tests/unit/test_endpoints_v2.py
@@ -0,0 +1,344 @@
+# Copyright 2012-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+
+import json
+import logging
+import os
+
+import pytest
+
+from botocore.endpoint_provider import (
+    EndpointProvider,
+    EndpointRule,
+    ErrorRule,
+    RuleCreator,
+    RuleSet,
+    RuleSetStandardLibary,
+    TreeRule,
+)
+from botocore.exceptions import EndpointResolutionError
+from botocore.loaders import Loader
+
+REGION_TEMPLATE = "{Region}"
+REGION_REF = {"ref": "Region"}
+BUCKET_ARN_REF = {"ref": "bucketArn"}
+PARSE_ARN_FUNC = {
+    "fn": "aws.parseArn",
+    "argv": [{"ref": "Bucket"}],
+    "assign": "bucketArn",
+}
+STRING_EQUALS_FUNC = {
+    "fn": "stringEquals",
+    "argv": [
+        {
+            "fn": "getAttr",
+            "argv": [BUCKET_ARN_REF, "region"],
+            "assign": "bucketRegion",
+        },
+        "",
+    ],
+}
+DNS_SUFFIX_TEMPLATE = "{PartitionResults#dnsSuffix}"
+URL_TEMPLATE = (
+    f"https://{REGION_TEMPLATE}.myGreatService.{DNS_SUFFIX_TEMPLATE}"
+)
+ENDPOINT_DICT = {
+    "url": URL_TEMPLATE,
+    "properties": {
+        "authSchemes": [
+            {
+                "signingName": "s3",
+                "signingScope": REGION_TEMPLATE,
+                "name": "s3v4",
+            }
+        ],
+    },
+    "headers": {
+        "x-amz-region-set": [
+            REGION_REF,
+            {
+                "fn": "getAttr",
+                "argv": [BUCKET_ARN_REF, "region"],
+            },
+            "us-east-2",
+        ],
+    },
+}
+
+
+@pytest.fixture(scope="module")
+def loader():
+    return Loader()
+
+
+@pytest.fixture(scope="module")
+def partitions(loader):
+    return loader.load_data("partitions")
+
+
+@pytest.fixture(scope="module")
+def rule_lib(partitions):
+    return RuleSetStandardLibary(partitions)
+
+
+@pytest.fixture(scope="module")
+def ruleset_dict():
+    path = os.path.join(
+        os.path.dirname(__file__),
+        "data",
+        "endpoints",
+        "valid-rules",
+        "deprecated-param.json",
+    )
+    with open(path) as f:
+        return json.load(f)
+
+
+@pytest.fixture(scope="module")
+def endpoint_provider(ruleset_dict, partitions):
+    return EndpointProvider(ruleset_dict, partitions)
+
+
+@pytest.fixture(scope="module")
+def endpoint_rule():
+    return EndpointRule(
+        endpoint=ENDPOINT_DICT,
+        conditions=[
+            PARSE_ARN_FUNC,
+            {
+                "fn": "not",
+                "argv": [STRING_EQUALS_FUNC],
+            },
+            {
+                "fn": "aws.partition",
+                "argv": [REGION_REF],
+                "assign": "PartitionResults",
+            },
+        ],
+    )
+
+
+def ruleset_testcases():
+    filenames = [
+        "aws-region",
+        "default-values",
+        "eventbridge",
+        "fns",
+        "headers",
+        "local-region-override",
+        "parse-arn",
+        "parse-url",
+        "substring",
+        "uri-encode",
+        "valid-hostlabel",
+    ]
+    error_cases = []
+    endpoint_cases = []
+    base_path = os.path.join(os.path.dirname(__file__), "data", "endpoints")
+    for name in filenames:
+
+        with open(os.path.join(base_path, "valid-rules", f"{name}.json")) as f:
+            ruleset = json.load(f)
+        with open(os.path.join(base_path, "test-cases", f"{name}.json")) as f:
+            tests = json.load(f)
+
+        for test in tests["testCases"]:
+            input_params = test["params"]
+            expected_object = test["expect"]
+            if "error" in expected_object:
+                error_cases.append(
+                    (ruleset, input_params, expected_object["error"])
+                )
+            elif "endpoint" in expected_object:
+                endpoint_cases.append(
+                    (ruleset, input_params, expected_object["endpoint"])
+                )
+            else:
+                raise ValueError("Expected `error` or `endpoint` in test case")
+    return error_cases, endpoint_cases
+
+
+ERROR_TEST_CASES, ENDPOINT_TEST_CASES = ruleset_testcases()
+
+
+@pytest.mark.parametrize(
+    "ruleset,input_params,expected_error",
+    ERROR_TEST_CASES,
+)
+def test_endpoint_resolution_raises(
+    partitions, ruleset, input_params, expected_error
+):
+    endpoint_provider = EndpointProvider(ruleset, partitions)
+    with pytest.raises(EndpointResolutionError) as exc_info:
+        endpoint_provider.resolve_endpoint(**input_params)
+    assert str(exc_info.value) == expected_error
+
+
+@pytest.mark.parametrize(
+    "ruleset,input_params,expected_endpoint",
+    ENDPOINT_TEST_CASES,
+)
+def test_endpoint_resolution(
+    partitions, ruleset, input_params, expected_endpoint
+):
+    endpoint_provider = EndpointProvider(ruleset, partitions)
+    endpoint = endpoint_provider.resolve_endpoint(**input_params)
+    assert endpoint.url == expected_endpoint["url"]
+    assert endpoint.properties == expected_endpoint.get("properties", {})
+    assert endpoint.headers == expected_endpoint.get("headers", {})
+
+
+def test_none_doesnt_use_default_partition(rule_lib):
+    assert rule_lib.aws_partition(None) is None
+
+
+def test_no_match_region_returns_default_partition(rule_lib):
+    partition_dict = rule_lib.aws_partition("invalid-region-42")
+    assert partition_dict['name'] == "aws"
+
+
+def test_invalid_arn_returns_none(rule_lib):
+    assert rule_lib.aws_parse_arn("arn:aws:this-is-not-an-arn:foo") is None
+
+
+def test_uri_encode_none_returns_none(rule_lib):
+    assert rule_lib.uri_encode(None) is None
+
+
+def test_parse_url_none_return_none(rule_lib):
+    assert rule_lib.parse_url(None) is None
+
+
+def test_string_equals_wrong_type_raises(rule_lib):
+    with pytest.raises(EndpointResolutionError) as exc_info:
+        rule_lib.string_equals(1, 2)
+    assert "Both values must be strings" in str(exc_info.value)
+
+
+def test_boolean_equals_wrong_type_raises(rule_lib):
+    with pytest.raises(EndpointResolutionError) as exc_info:
+        rule_lib.boolean_equals(1, 2)
+    assert "Both arguments must be bools" in str(exc_info.value)
+
+
+def test_substring_wrong_type_raises(rule_lib):
+    with pytest.raises(EndpointResolutionError) as exc_info:
+        rule_lib.substring(["h", "e", "l", "l", "o"], 0, 5, False)
+    assert "Input must be a string" in str(exc_info.value)
+
+
+def test_creator_unknown_type_raises():
+    with pytest.raises(EndpointResolutionError) as exc_info:
+        RuleCreator.create(type="foo")
+    assert "Unknown rule type: foo." in str(exc_info.value)
+
+
+def test_parameter_wrong_type_raises(endpoint_provider):
+    param = endpoint_provider.ruleset.parameters["Region"]
+    with pytest.raises(EndpointResolutionError) as exc_info:
+        param.validate_input(1)
+    assert "Value (Region) is the wrong type" in str(exc_info.value)
+
+
+def test_deprecated_parameter_logs(endpoint_provider, caplog):
+    caplog.set_level(logging.INFO)
+    param = endpoint_provider.ruleset.parameters["Region"]
+    param.validate_input("foo")
+    assert "Region has been deprecated." in caplog.text
+
+
+def test_no_endpoint_found_error(endpoint_provider):
+    with pytest.raises(EndpointResolutionError) as exc_info:
+        endpoint_provider.resolve_endpoint(
+            **{"Endpoint": "mygreatendpoint.com", "Bucket": "mybucket"}
+        )
+    assert "No endpoint found for parameters" in str(exc_info.value)
+
+
+@pytest.mark.parametrize(
+    "rule_dict,expected_rule_type",
+    [
+        (
+            {
+                "type": "endpoint",
+                "conditions": [],
+                "endpoint": {
+                    "url": (
+                        "https://{Region}.myGreatService."
+                        "{PartitionResult#dualStackDnsSuffix}"
+                    ),
+                    "properties": {},
+                    "headers": {},
+                },
+            },
+            EndpointRule,
+        ),
+        (
+            {
+                "type": "error",
+                "conditions": [],
+                "error": (
+                    "Dualstack is enabled but this partition "
+                    "does not support DualStack"
+                ),
+            },
+            ErrorRule,
+        ),
+        ({"type": "tree", "conditions": [], "rules": []}, TreeRule),
+    ],
+)
+def test_rule_creation(rule_dict, expected_rule_type):
+    rule = RuleCreator.create(**rule_dict)
+    assert isinstance(rule, expected_rule_type)
+
+
+def test_assign_existing_scope_var_raises(rule_lib):
+    rule = EndpointRule(
+        conditions=[
+            {
+                'fn': 'aws.parseArn',
+                'argv': ['{Bucket}'],
+                'assign': 'bucketArn',
+            },
+            {
+                'fn': 'aws.parseArn',
+                'argv': ['{Bucket}'],
+                'assign': 'bucketArn',
+            },
+        ],
+        endpoint={'url': 'foo.bar'},
+    )
+    with pytest.raises(EndpointResolutionError) as exc_info:
+        rule.evaluate_conditions(
+            scope_vars={
+                'Bucket': 'arn:aws:s3:us-east-1:123456789012:mybucket'
+            },
+            rule_lib=rule_lib,
+        )
+    assert str(exc_info.value) == (
+        "Assignment bucketArn already exists in "
+        "scoped variables and cannot be overwritten"
+    )
+
+
+def test_ruleset_unknown_parameter_type_raises(partitions):
+    with pytest.raises(EndpointResolutionError) as exc_info:
+        RuleSet(
+            version='1.0',
+            parameters={
+                'Bucket': {"type": "list"},
+            },
+            rules=[],
+            partitions=partitions,
+        )
+    assert "Unknown parameter type: list." in str(exc_info.value)