diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a3c15ea6f..258d8ad7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -135,9 +135,12 @@ jobs: - os: ubuntu-20.04 archs: aarch64 build: musllinux - - os: macos-12 + - os: macos-14 archs: arm64 build: "" + - os: windows-2019 + archs: ARM64 + build: "" steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 diff --git a/.gitignore b/.gitignore index 763596319..5ed2af480 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ src/pact/bin src/pact/data +# Test outputs +examples/tests/pacts + # Version is determined from the VCS src/pact/__version__.py diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml index 2405a49a9..1b39fa089 100644 --- a/examples/docker-compose.yml +++ b/examples/docker-compose.yml @@ -15,7 +15,8 @@ services: broker: image: pactfoundation/pact-broker:latest-multi depends_on: - - postgres + postgres: + condition: service_healthy ports: - "9292:9292" restart: always @@ -41,3 +42,4 @@ services: interval: 1s timeout: 2s retries: 5 + start_period: 30s diff --git a/examples/src/message.py b/examples/src/message.py index cab137ec0..815719903 100644 --- a/examples/src/message.py +++ b/examples/src/message.py @@ -14,7 +14,22 @@ class Filesystem: - """Filesystem interface.""" + """ + Filesystem interface. + + In practice, the handler would process messages and perform some actions on + other systems, whether that be a database, a filesystem, or some other + service. This capability would typically be offered by some library; + however, when running tests, we typically wish to avoid actually interacting + with this external service. + + In order to avoid side effects while testing, the test setup should mock out + the calls to the external service. + + This class provides a simple dummy filesystem interface (which evidently + would fail if actually used), and serves to demonstrate how to mock out + external services when testing. + """ def __init__(self) -> None: """Initialize the filesystem connection.""" diff --git a/examples/tests/test_01_provider_fastapi.py b/examples/tests/test_01_provider_fastapi.py index 7ccd3128a..a95b5b5f8 100644 --- a/examples/tests/test_01_provider_fastapi.py +++ b/examples/tests/test_01_provider_fastapi.py @@ -24,6 +24,7 @@ from __future__ import annotations +import time from multiprocessing import Process from typing import Any, Dict, Generator, Union from unittest.mock import MagicMock @@ -93,6 +94,7 @@ def verifier() -> Generator[Verifier, Any, None]: provider_base_url=str(PROVIDER_URL), ) proc.start() + time.sleep(2) yield verifier proc.kill() diff --git a/examples/tests/test_01_provider_flask.py b/examples/tests/test_01_provider_flask.py index ba5c39d43..b7082dabb 100644 --- a/examples/tests/test_01_provider_flask.py +++ b/examples/tests/test_01_provider_flask.py @@ -24,6 +24,7 @@ from __future__ import annotations +import time from multiprocessing import Process from typing import Any, Dict, Generator, Union from unittest.mock import MagicMock @@ -81,6 +82,7 @@ def verifier() -> Generator[Verifier, Any, None]: provider_base_url=str(PROVIDER_URL), ) proc.start() + time.sleep(2) yield verifier proc.kill() diff --git a/examples/tests/v3/__init__.py b/examples/tests/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/tests/v3/test_01_message_consumer.py b/examples/tests/v3/test_01_message_consumer.py new file mode 100644 index 000000000..9016be9b8 --- /dev/null +++ b/examples/tests/v3/test_01_message_consumer.py @@ -0,0 +1,170 @@ +""" +Consumer test of example message handler using the v3 API. + +This test will create a pact between the message handler +and the message provider. +""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generator, +) +from unittest.mock import MagicMock + +import pytest + +from examples.src.message import Handler +from pact.v3.pact import Pact + +if TYPE_CHECKING: + from collections.abc import Callable + + +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="module") +def pact() -> Generator[Pact, None, None]: + """ + Set up Message Pact Consumer. + + This fixtures sets up the Message Pact consumer and the pact it has with a + provider. The consumer defines the expected messages it will receive from + the provider, and the Python test suite verifies that the correct actions + are taken. + + The verify method takes a function as an argument. This function + will be called with one or two arguments - the value of `with_body` and + the contents of `with_metadata` if provided. + + If the function under test does not take those parameters, you can create + a wrapper function to convert the pact parameters into the values + expected by your function. + + + For each interaction, the consumer defines the following: + + ```python + ( + pact = Pact("consumer name", "provider name") + processed_messages: list[MessagePact.MessagePactResult] = pact \ + .with_specification("V3") + .upon_receiving("a request", "Async") \ + .given("a request to write test.txt") \ + .with_body(msg) \ + .with_metadata({"Content-Type": "application/json"}) + .verify(pact_handler) + ) + + ``` + """ + pact_dir = Path(Path(__file__).parent.parent / "pacts") + pact = Pact("v3_message_consumer", "v3_message_provider") + log.info("Creating Message Pact with V3 specification") + yield pact.with_specification("V3") + pact.write_file(pact_dir, overwrite=True) + + +@pytest.fixture() +def handler() -> Handler: + """ + Fixture for the Handler. + + This fixture mocks the filesystem calls in the handler, so that we can + verify that the handler is calling the filesystem correctly. + """ + handler = Handler() + handler.fs = MagicMock() + handler.fs.write.return_value = None + handler.fs.read.return_value = "Hello world!" + return handler + + +@pytest.fixture() +def verifier( + handler: Handler, +) -> Generator[Callable[[str | bytes | None, Dict[str, Any]], None], Any, None]: + """ + Verifier function for the Pact. + + This function is passed to the `verify` method of the Pact object. It is + responsible for taking in the messages (along with the context/metadata) + and ensuring that the consumer is able to process the message correctly. + + In our case, we deserialize the message and pass it to the (pre-mocked) + handler for processing. We then verify that the underlying filesystem + calls were made as expected. + """ + assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked" + + def _verifier(msg: str | bytes | None, context: Dict[str, Any]) -> None: + assert msg is not None, "Message is None" + data = json.loads(msg) + log.info( + "Processing message: ", + extra={"input": msg, "processed_message": data, "context": context}, + ) + handler.process(data) + + yield _verifier + + assert handler.fs.mock_calls, "Handler did not call the filesystem" + + +def test_async_message_handler_write( + pact: Pact, + handler: Handler, + verifier: Callable[[str | bytes | None, Dict[str, Any]], None], +) -> None: + """ + Create a pact between the message handler and the message provider. + """ + assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked" + + ( + pact.upon_receiving("a write request", "Async") + .given("a request to write test.txt") + .with_body( + json.dumps({ + "action": "WRITE", + "path": "my_file.txt", + "contents": "Hello, world!", + }) + ) + ) + pact.verify(verifier, "Async") + + handler.fs.write.assert_called_once_with("my_file.txt", "Hello, world!") + + +def test_async_message_handler_read( + pact: Pact, + handler: Handler, + verifier: Callable[[str | bytes | None, Dict[str, Any]], None], +) -> None: + """ + Create a pact between the message handler and the message provider. + """ + assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked" + + ( + pact.upon_receiving("a read request", "Async") + .given("a request to read test.txt") + .with_body( + json.dumps({ + "action": "READ", + "path": "my_file.txt", + "contents": "Hello, world!", + }) + ) + ) + pact.verify(verifier, "Async") + + handler.fs.read.assert_called_once_with("my_file.txt") diff --git a/hatch_build.py b/hatch_build.py index 9842fb6a4..ab5e84e0f 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -36,7 +36,7 @@ # Latest version available at: # https://github.com/pact-foundation/pact-reference/releases -PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.19") +PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.21") PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{machine}.{ext}" @@ -256,7 +256,7 @@ def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 if platform.startswith("macosx"): os = "macos" if platform.endswith("arm64"): - machine = "aarch64-apple-darwin" + machine = "aarch64" elif platform.endswith("x86_64"): machine = "x86_64" else: @@ -274,6 +274,8 @@ def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 if platform.endswith("amd64"): machine = "x86_64" + elif platform.endswith(("arm64", "aarch64")): + machine = "aarch64" else: raise UnsupportedPlatformError(platform) return PACT_LIB_URL.format( diff --git a/pyproject.toml b/pyproject.toml index a8ef954c2..7c90e1875 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -187,8 +187,10 @@ addopts = [ "--cov-report=xml", ] filterwarnings = [ + "ignore::DeprecationWarning:examples", "ignore::DeprecationWarning:pact", "ignore::DeprecationWarning:tests", + "ignore::PendingDeprecationWarning:examples", "ignore::PendingDeprecationWarning:pact", "ignore::PendingDeprecationWarning:tests", ] diff --git a/src/pact/v3/__init__.py b/src/pact/v3/__init__.py index 91329f2dc..15a207faa 100644 --- a/src/pact/v3/__init__.py +++ b/src/pact/v3/__init__.py @@ -70,8 +70,8 @@ import warnings -from pact.v3.pact import Pact # noqa: F401 -from pact.v3.verifier import Verifier # noqa: F401 +from pact.v3.pact import Pact +from pact.v3.verifier import Verifier warnings.warn( "The `pact.v3` module is not yet stable. Use at your own risk, and expect " @@ -79,3 +79,5 @@ stacklevel=2, category=ImportWarning, ) + +__all__ = ["Pact", "Verifier"] diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index e23d94b86..8108dcee4 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -80,6 +80,9 @@ # ruff: noqa: SLF001 # private-member-access, as we need access to other handles' internal # references, without exposing them to the user. +# pyright: reportPrivateUsage=false +# Ignore private member access, as we frequently need to use the +# object's underlying pointer stored in `_ptr`. from __future__ import annotations @@ -89,7 +92,8 @@ import typing import warnings from enum import Enum -from typing import TYPE_CHECKING, Any, List +from typing import TYPE_CHECKING, Any, List, Literal, Tuple +from typing import Generator as GeneratorType from pact.v3._ffi import ffi, lib # type: ignore[import] @@ -103,232 +107,396 @@ logger = logging.getLogger(__name__) +################################################################################ +# Type aliases +################################################################################ +# The following type aliases provide a nicer interface for end-users of the +# library, especially when it comes to [`Enum`][Enum] classes which offers +# support for string literals as alternative values. + +GeneratorCategoryOptions = Literal[ + "METHOD", "method", + "PATH", "path", + "HEADER", "header", + "QUERY", "query", + "BODY", "body", + "STATUS", "status", + "METADATA", "metadata", +] # fmt: skip +""" +Generator Category Options. + +Type alias for the string literals which represent the Generator Category +Options. +""" + +MatchingRuleCategoryOptions = Literal[ + "METHOD", "method", + "PATH", "path", + "HEADER", "header", + "QUERY", "query", + "BODY", "body", + "STATUS", "status", + "CONTENTS", "contents", + "METADATA", "metadata", +] # fmt: skip + +################################################################################ +# Classes +################################################################################ # The follow types are classes defined in the Rust code. Ultimately, a Python # alternative should be implemented, but for now, the follow lines only serve # to inform the type checker of the existence of these types. -class AsynchronousMessage: ... - +class AsynchronousMessage: + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: + """ + Initialise a new Asynchronous Message. -class Consumer: ... + Args: + ptr: + CFFI data structure. + owned: + Whether the message is owned by something else or not. This + determines whether the message should be freed when the Python + object is destroyed. -class Generator: ... + Raises: + TypeError: + If the `ptr` is not a `struct AsynchronousMessage`. + """ + if ffi.typeof(ptr).cname != "struct AsynchronousMessage *": + msg = ( + "ptr must be a struct AsynchronousMessage, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + self._owned = owned + def __str__(self) -> str: + """ + Nice string representation. + """ + return "AsynchronousMessage" -class GeneratorCategoryIterator: ... + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"AsynchronousMessage({self._ptr!r})" + def __del__(self) -> None: + """ + Destructor for the AsynchronousMessage. + """ + if not self._owned: + async_message_delete(self) -class GeneratorKeyValuePair: ... + @property + def description(self) -> str: + """ + Description of this message interaction. + This needs to be unique in the pact file. + """ + return async_message_get_description(self) -class HttpRequest: ... + def provider_states(self) -> GeneratorType[ProviderState, None, None]: + """ + Optional provider state for the interaction. + """ + yield from async_message_get_provider_state_iter(self) + return # Ensures that the parent object outlives the generator + @property + def contents(self) -> MessageContents | None: + """ + The contents of the message. -class HttpResponse: ... + This may be `None` if the message has no contents. + """ + return async_message_generate_contents(self) -class InteractionHandle: - """ - Handle to a HTTP Interaction. +class Consumer: ... - [Rust - `InteractionHandle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/mock_server/handles/struct.InteractionHandle.html) - """ - def __init__(self, ref: int) -> None: +class Generator: + def __init__(self, ptr: cffi.FFI.CData) -> None: """ - Initialise a new Interaction Handle. + Initialise a generator value. Args: - ref: - Reference to the Interaction Handle. + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct Generator`. """ - self._ref: int = ref + if ffi.typeof(ptr).cname != "struct Generator *": + msg = "ptr must be a struct Generator, got" f" {ffi.typeof(ptr).cname}" + raise TypeError(msg) + self._ptr = ptr def __str__(self) -> str: """ - String representation of the Interaction Handle. + Nice string representation. """ - return f"InteractionHandle({self._ref})" + return "Generator" def __repr__(self) -> str: """ - String representation of the Interaction Handle. + Debugging representation. """ - return f"InteractionHandle({self._ref!r})" - - -class MatchingRule: ... - - -class MatchingRuleCategoryIterator: ... - - -class MatchingRuleDefinitionResult: ... - - -class MatchingRuleIterator: ... - - -class MatchingRuleKeyValuePair: ... + return f"Generator({self._ptr!r})" + def __del__(self) -> None: + """ + Destructor for the Generator. + """ -class MatchingRuleResult: ... + @property + def json(self) -> dict[str, Any]: + """ + Dictionary representation of the generator. + """ + return json.loads(generator_to_json(self)) + def generate_string(self, context: dict[str, Any] | None = None) -> str: + """ + Generate a string from the generator. -class Message: ... + Args: + context: + JSON payload containing any generator context. For example: + - The context for a `MockServerURL` generator should contain + details about the running mock server. + - The context for a `ProviderStateGenerator` should contain + the values returned from the provider state callback + function. + """ + return generator_generate_string(self, json.dumps(context or {})) -class MessageContents: ... + def generate_integer(self, context: dict[str, Any] | None = None) -> int: + """ + Generate an integer from the generator. + Args: + context: + JSON payload containing any generator context. For example: -class MessageHandle: ... + - The context for a `ProviderStateGenerator` should contain + the values returned from the provider state callback + function. + """ + return generator_generate_integer(self, json.dumps(context or {})) -class MessageMetadataIterator: ... +class GeneratorCategoryIterator: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new generator category iterator. + Args: + ptr: + CFFI data structure. -class MessageMetadataPair: ... + Raises: + TypeError: + If the `ptr` is not a `struct GeneratorCategoryIterator`. + """ + if ffi.typeof(ptr).cname != "struct GeneratorCategoryIterator *": + msg = ( + "ptr must be a struct GeneratorCategoryIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + def __str__(self) -> str: + """ + Nice string representation. + """ + return "GeneratorCategoryIterator" -class MessagePact: ... + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"GeneratorCategoryIterator({self._ptr!r})" + def __del__(self) -> None: + """ + Destructor for the GeneratorCategoryIterator. + """ + generators_iter_delete(self) -class MessagePactHandle: ... + def __iter__(self) -> Self: + """ + Return the iterator itself. + """ + return self + def __next__(self) -> GeneratorKeyValuePair: + """ + Get the next generator category from the iterator. + """ + return generators_iter_next(self) -class MessagePactMessageIterator: ... +class GeneratorKeyValuePair: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new key-value generator pair. -class MessagePactMetadataIterator: ... + Args: + ptr: + CFFI data structure. + Raises: + TypeError: + If the `ptr` is not a `struct GeneratorKeyValuePair`. + """ + if ffi.typeof(ptr).cname != "struct GeneratorKeyValuePair *": + msg = ( + "ptr must be a struct GeneratorKeyValuePair, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr -class MessagePactMetadataTriple: ... + def __str__(self) -> str: + """ + Nice string representation. + """ + return "GeneratorKeyValuePair" + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"GeneratorKeyValuePair({self._ptr!r})" -class Mismatch: ... + def __del__(self) -> None: + """ + Destructor for the GeneratorKeyValuePair. + """ + generators_iter_pair_delete(self) + @property + def path(self) -> str: + """ + Generator path. + """ + s = ffi.string(self._ptr.path) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s -class Mismatches: ... + @property + def generator(self) -> Generator: + """ + Generator value. + """ + return Generator(self._ptr.generator) # type: ignore[attr-defined] -class MismatchesIterator: ... +class HttpRequest: ... -class Pact: ... +class HttpResponse: ... -class PactHandle: +class InteractionHandle: """ - Handle to a Pact. + Handle to a HTTP Interaction. [Rust - `PactHandle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/mock_server/handles/struct.PactHandle.html) + `InteractionHandle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/handles/struct.InteractionHandle.html) """ def __init__(self, ref: int) -> None: """ - Initialise a new Pact Handle. + Initialise a new Interaction Handle. Args: ref: - Rust library reference to the Pact Handle. + Reference to the Interaction Handle. """ self._ref: int = ref - def __del__(self) -> None: - """ - Destructor for the Pact Handle. - """ - cleanup_plugins(self) - free_pact_handle(self) - def __str__(self) -> str: """ - String representation of the Pact Handle. + String representation of the Interaction Handle. """ - return f"PactHandle({self._ref})" + return f"InteractionHandle({self._ref})" def __repr__(self) -> str: """ - String representation of the Pact Handle. + String representation of the Interaction Handle. """ - return f"PactHandle({self._ref!r})" - - -class PactServerHandle: - """ - Handle to a Pact Server. - - This does not have an exact correspondance in the Rust library. It is used - to manage the lifecycle of the mock server. - - # Implementation Notes + return f"InteractionHandle({self._ref!r})" - The Rust library uses the port number as a unique identifier, in much the - same was as it uses a wrapped integer for the Pact handle. - """ - def __init__(self, ref: int) -> None: +class MatchingRule: + def __init__(self, ptr: cffi.FFI.CData) -> None: """ - Initialise a new Pact Server Handle. + Initialise a new key-value generator pair. Args: - ref: - Rust library reference to the Pact Server. - """ - self._ref: int = ref + ptr: + CFFI data structure. - def __del__(self) -> None: - """ - Destructor for the Pact Server Handle. + Raises: + TypeError: + If the `ptr` is not a `struct MatchingRule`. """ - cleanup_mock_server(self) + if ffi.typeof(ptr).cname != "struct MatchingRule *": + msg = "ptr must be a struct MatchingRule, got" f" {ffi.typeof(ptr).cname}" + raise TypeError(msg) + self._ptr = ptr def __str__(self) -> str: """ - String representation of the Pact Server Handle. + Nice string representation. """ - return f"PactServerHandle({self._ref})" + return "MatchingRule" def __repr__(self) -> str: """ - String representation of the Pact Server Handle. + Debugging representation. """ - return f"PactServerHandle({self._ref!r})" + return f"MatchingRule({self._ptr!r})" @property - def port(self) -> int: + def json(self) -> dict[str, Any]: """ - Port on which the Pact Server is running. + Dictionary representation of the matching rule. """ - return self._ref - + return json.loads(matching_rule_to_json(self)) -class PactInteraction: ... - - -class PactInteractionIterator: - """ - Iterator over a Pact's interactions. - - Interactions encompasses all types of interactions, including HTTP - interactions and messages. - """ +class MatchingRuleCategoryIterator: def __init__(self, ptr: cffi.FFI.CData) -> None: """ - Initialise a new Pact Interaction Iterator. + Initialise a new key-value generator pair. Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MatchingRuleCategoryIterator`. """ - if ffi.typeof(ptr).cname != "struct PactInteractionIterator *": + if ffi.typeof(ptr).cname != "struct MatchingRuleCategoryIterator *": msg = ( - "ptr must be a struct PactInteractionIterator, got" + "ptr must be a struct MatchingRuleCategoryIterator, got" f" {ffi.typeof(ptr).cname}" ) raise TypeError(msg) @@ -338,43 +506,56 @@ def __str__(self) -> str: """ Nice string representation. """ - return "PactInteractionIterator" + return "MatchingRuleCategoryIterator" def __repr__(self) -> str: """ Debugging representation. """ - return f"PactInteractionIterator({self._ptr!r})" + return f"MatchingRuleCategoryIterator({self._ptr!r})" def __del__(self) -> None: """ - Destructor for the Pact Interaction Iterator. + Destructor for the MatchingRuleCategoryIterator. """ - pact_interaction_iter_delete(self) + matching_rules_iter_delete(self) - def __next__(self) -> PactInteraction: + def __iter__(self) -> Self: """ - Get the next interaction from the iterator. + Return the iterator itself. """ - return pact_interaction_iter_next(self) + return self + def __next__(self) -> MatchingRuleKeyValuePair: + """ + Get the next generator category from the iterator. + """ + return matching_rules_iter_next(self) -class PactMessageIterator: - """ - Iterator over a Pact's asynchronous messages. - """ +class MatchingRuleDefinitionResult: ... + + +class MatchingRuleIterator: ... + + +class MatchingRuleKeyValuePair: def __init__(self, ptr: cffi.FFI.CData) -> None: """ - Initialise a new Pact Message Iterator. + Initialise a new key-value generator pair. Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MatchingRuleKeyValuePair`. """ - if ffi.typeof(ptr).cname != "struct PactMessageIterator *": + if ffi.typeof(ptr).cname != "struct MatchingRuleKeyValuePair *": msg = ( - f"ptr must be a struct PactMessageIterator, got {ffi.typeof(ptr).cname}" + "ptr must be a struct MatchingRuleKeyValuePair, got" + f" {ffi.typeof(ptr).cname}" ) raise TypeError(msg) self._ptr = ptr @@ -383,101 +564,148 @@ def __str__(self) -> str: """ Nice string representation. """ - return "PactMessageIterator" + return "MatchingRuleKeyValuePair" def __repr__(self) -> str: """ Debugging representation. """ - return f"PactMessageIterator({self._ptr!r})" + return f"MatchingRuleKeyValuePair({self._ptr!r})" def __del__(self) -> None: """ - Destructor for the Pact Message Iterator. + Destructor for the MatchingRuleKeyValuePair. """ - pact_message_iter_delete(self) + matching_rules_iter_pair_delete(self) - def __iter__(self) -> Self: + @property + def path(self) -> str: """ - Return the iterator itself. + Matching Rule path. """ - return self + s = ffi.string(self._ptr.path) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s - def __next__(self) -> Message: + @property + def matching_rule(self) -> MatchingRule: """ - Get the next message from the iterator. + Matching Rule value. """ - return pact_message_iter_next(self) + return MatchingRule(self._ptr.matching_rule) # type: ignore[attr-defined] -class PactSyncHttpIterator: - """ - Iterator over a Pact's synchronous HTTP interactions. - """ +class MatchingRuleResult: ... - def __init__(self, ptr: cffi.FFI.CData) -> None: + +class MessageContents: + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = True) -> None: """ - Initialise a new Pact Synchronous HTTP Iterator. + Initialise a Message Contents. Args: ptr: CFFI data structure. + + owned: + Whether the message is owned by something else or not. This + determines whether the message should be freed when the Python + object is destroyed. + + Raises: + TypeError: + If the `ptr` is not a `struct MessageContents`. """ - if ffi.typeof(ptr).cname != "struct PactSyncHttpIterator *": + if ffi.typeof(ptr).cname != "struct MessageContents *": msg = ( - "ptr must be a struct PactSyncHttpIterator, got" - f" {ffi.typeof(ptr).cname}" + "ptr must be a struct MessageContents, got" f" {ffi.typeof(ptr).cname}" ) raise TypeError(msg) self._ptr = ptr + self._owned = owned def __str__(self) -> str: """ Nice string representation. """ - return "PactSyncHttpIterator" + return "MessageContents" def __repr__(self) -> str: """ Debugging representation. """ - return f"PactSyncHttpIterator({self._ptr!r})" + return f"MessageContents({self._ptr!r})" def __del__(self) -> None: """ - Destructor for the Pact Synchronous HTTP Iterator. + Destructor for the MessageContents. """ - pact_sync_http_iter_delete(self) + if not self._owned: + message_contents_delete(self) - def __iter__(self) -> Self: + @property + def contents(self) -> str | bytes | None: """ - Return the iterator itself. + Get the contents of the message. """ - return self + return message_contents_get_contents_str( + self + ) or message_contents_get_contents_bin(self) - def __next__(self) -> SynchronousHttp: + @property + def metadata(self) -> GeneratorType[MessageMetadataPair, None, None]: """ - Get the next message from the iterator. + Get the metadata for the message contents. """ - return pact_sync_http_iter_next(self) + yield from message_contents_get_metadata_iter(self) + return # Ensures that the parent object outlives the generator + def matching_rules( + self, + category: MatchingRuleCategoryOptions | MatchingRuleCategory, + ) -> GeneratorType[MatchingRuleKeyValuePair, None, None]: + """ + Get the matching rules for the message contents. + """ + if isinstance(category, str): + category = MatchingRuleCategory(category.upper()) + yield from message_contents_get_matching_rule_iter(self, category) + return # Ensures that the parent object outlives the generator -class PactSyncMessageIterator: + def generators( + self, + category: GeneratorCategoryOptions | GeneratorCategory, + ) -> GeneratorType[GeneratorKeyValuePair, None, None]: + """ + Get the generators for the message contents. + """ + if isinstance(category, str): + category = GeneratorCategory(category.upper()) + yield from message_contents_get_generators_iter(self, category) + return # Ensures that the parent object outlives the generator + + +class MessageMetadataIterator: """ - Iterator over a Pact's synchronous messages. + Iterator over an interaction's metadata. """ def __init__(self, ptr: cffi.FFI.CData) -> None: """ - Initialise a new Pact Synchronous Message Iterator. + Initialise a new Message Metadata Iterator. Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MessageMetadataIterator`. """ - if ffi.typeof(ptr).cname != "struct PactSyncMessageIterator *": + if ffi.typeof(ptr).cname != "struct MessageMetadataIterator *": msg = ( - "ptr must be a struct PactSyncMessageIterator, got" + "ptr must be a struct MessageMetadataIterator, got" f" {ffi.typeof(ptr).cname}" ) raise TypeError(msg) @@ -487,19 +715,19 @@ def __str__(self) -> str: """ Nice string representation. """ - return "PactSyncMessageIterator" + return "MessageMetadataIterator" def __repr__(self) -> str: """ Debugging representation. """ - return f"PactSyncMessageIterator({self._ptr!r})" + return f"MessageMetadataIterator({self._ptr!r})" def __del__(self) -> None: """ - Destructor for the Pact Synchronous Message Iterator. + Destructor for the Pact Interaction Iterator. """ - pact_sync_message_iter_delete(self) + message_metadata_iter_delete(self) def __iter__(self) -> Self: """ @@ -507,1723 +735,1651 @@ def __iter__(self) -> Self: """ return self - def __next__(self) -> SynchronousMessage: + def __next__(self) -> MessageMetadataPair: """ - Get the next message from the iterator. + Get the next interaction from the iterator. """ - return pact_sync_message_iter_next(self) + return message_metadata_iter_next(self) -class Provider: ... +class MessageMetadataPair: + """ + A metadata key-value pair. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Message Metadata Pair. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct MessageMetadataPair`. + """ + if ffi.typeof(ptr).cname != "struct MessageMetadataPair *": + msg = ( + "ptr must be a struct MessageMetadataPair, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + + def __str__(self) -> str: + """ + Nice string representation. + """ + return "MessageMetadataPair" + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"MessageMetadataPair({self._ptr!r})" -class ProviderState: ... + def __del__(self) -> None: + """ + Destructor for the Pact Interaction Iterator. + """ + message_metadata_pair_delete(self) + @property + def key(self) -> str: + """ + Metadata key. + """ + s = ffi.string(self._ptr.key) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s -class ProviderStateIterator: ... + @property + def value(self) -> str: + """ + Metadata value. + """ + s = ffi.string(self._ptr.value) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s -class ProviderStateParamIterator: ... +class Mismatch: ... -class ProviderStateParamPair: ... +class Mismatches: ... -class SynchronousHttp: ... +class MismatchesIterator: ... -class SynchronousMessage: ... +class Pact: ... -class VerifierHandle: +class PactAsyncMessageIterator: """ - Handle to a Verifier. - - [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/verifier/handle/struct.VerifierHandle.html) + Iterator over a Pact's asynchronous messages. """ - def __init__(self, ref: cffi.FFI.CData) -> None: + def __init__(self, ptr: cffi.FFI.CData) -> None: """ - Initialise a new Verifier Handle. + Initialise a new Pact Asynchronous Message Iterator. Args: - ref: - Rust library reference to the Verifier Handle. - """ - self._ref = ref + ptr: + CFFI data structure. - def __del__(self) -> None: - """ - Destructor for the Verifier Handle. + Raises: + TypeError: + If the `ptr` is not a `struct PactAsyncMessageIterator`. """ - verifier_shutdown(self) + if ffi.typeof(ptr).cname != "struct PactAsyncMessageIterator *": + msg = ( + "ptr must be a struct PactAsyncMessageIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr def __str__(self) -> str: """ - String representation of the Verifier Handle. + Nice string representation. """ - return f"VerifierHandle({hex(id(self._ref))})" + return "PactAsyncMessageIterator" def __repr__(self) -> str: """ - String representation of the Verifier Handle. + Debugging representation. """ - return f"" - - -class ExpressionValueType(Enum): - """ - Expression Value Type. - - [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/models/expressions/enum.ExpressionValueType.html) - """ + return f"PactAsyncMessageIterator({self._ptr!r})" - UNKNOWN = lib.ExpressionValueType_Unknown - STRING = lib.ExpressionValueType_String - NUMBER = lib.ExpressionValueType_Number - INTEGER = lib.ExpressionValueType_Integer - DECIMAL = lib.ExpressionValueType_Decimal - BOOLEAN = lib.ExpressionValueType_Boolean + def __del__(self) -> None: + """ + Destructor for the Pact Synchronous Message Iterator. + """ + pact_async_message_iter_delete(self) - def __str__(self) -> str: + def __iter__(self) -> Self: """ - Informal string representation of the Expression Value Type. + Return the iterator itself. """ - return self.name + return self - def __repr__(self) -> str: + def __next__(self) -> AsynchronousMessage: """ - Information-rich string representation of the Expression Value Type. + Get the next message from the iterator. """ - return f"ExpressionValueType.{self.name}" + return pact_async_message_iter_next(self) -class GeneratorCategory(Enum): +class PactHandle: """ - Generator Category. + Handle to a Pact. - [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/models/generators/enum.GeneratorCategory.html) + [Rust + `PactHandle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/handles/struct.PactHandle.html) """ - METHOD = lib.GeneratorCategory_METHOD - PATH = lib.GeneratorCategory_PATH - HEADER = lib.GeneratorCategory_HEADER - QUERY = lib.GeneratorCategory_QUERY - BODY = lib.GeneratorCategory_BODY - STATUS = lib.GeneratorCategory_STATUS - METADATA = lib.GeneratorCategory_METADATA + def __init__(self, ref: int) -> None: + """ + Initialise a new Pact Handle. + + Args: + ref: + Rust library reference to the Pact Handle. + """ + self._ref: int = ref + + def __del__(self) -> None: + """ + Destructor for the Pact Handle. + """ + cleanup_plugins(self) + free_pact_handle(self) def __str__(self) -> str: """ - Informal string representation of the Generator Category. + String representation of the Pact Handle. """ - return self.name + return f"PactHandle({self._ref})" def __repr__(self) -> str: """ - Information-rich string representation of the Generator Category. + String representation of the Pact Handle. """ - return f"GeneratorCategory.{self.name}" + return f"PactHandle({self._ref!r})" -class InteractionPart(Enum): +class PactServerHandle: """ - Interaction Part. + Handle to a Pact Server. - [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/mock_server/handles/enum.InteractionPart.html) - """ + This does not have an exact correspondance in the Rust library. It is used + to manage the lifecycle of the mock server. - REQUEST = lib.InteractionPart_Request - RESPONSE = lib.InteractionPart_Response + # Implementation Notes - def __str__(self) -> str: + The Rust library uses the port number as a unique identifier, in much the + same was as it uses a wrapped integer for the Pact handle. + """ + + def __init__(self, ref: int) -> None: """ - Informal string representation of the Interaction Part. + Initialise a new Pact Server Handle. + + Args: + ref: + Rust library reference to the Pact Server. """ - return self.name + self._ref: int = ref - def __repr__(self) -> str: + def __del__(self) -> None: """ - Information-rich string representation of the Interaction Part. + Destructor for the Pact Server Handle. """ - return f"InteractionPath.{self.name}" - - -class LevelFilter(Enum): - """Level Filter.""" - - OFF = lib.LevelFilter_Off - ERROR = lib.LevelFilter_Error - WARN = lib.LevelFilter_Warn - INFO = lib.LevelFilter_Info - DEBUG = lib.LevelFilter_Debug - TRACE = lib.LevelFilter_Trace + cleanup_mock_server(self) def __str__(self) -> str: """ - Informal string representation of the Level Filter. + String representation of the Pact Server Handle. """ - return self.name + return f"PactServerHandle({self._ref})" def __repr__(self) -> str: """ - Information-rich string representation of the Level Filter. + String representation of the Pact Server Handle. """ - return f"LevelFilter.{self.name}" + return f"PactServerHandle({self._ref!r})" + @property + def port(self) -> int: + """ + Port on which the Pact Server is running. + """ + return self._ref -class MatchingRuleCategory(Enum): - """ - Matching Rule Category. - [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) +class PactInteraction: ... + + +class PactInteractionIterator: """ + Iterator over a Pact's interactions. - METHOD = lib.MatchingRuleCategory_METHOD - PATH = lib.MatchingRuleCategory_PATH - HEADER = lib.MatchingRuleCategory_HEADER - QUERY = lib.MatchingRuleCategory_QUERY - BODY = lib.MatchingRuleCategory_BODY - STATUS = lib.MatchingRuleCategory_STATUS - CONTENST = lib.MatchingRuleCategory_CONTENTS - METADATA = lib.MatchingRuleCategory_METADATA + Interactions encompasses all types of interactions, including HTTP + interactions and messages. + """ + + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Pact Interaction Iterator. + + Args: + ptr: + CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct PactInteractionIterator`. + """ + if ffi.typeof(ptr).cname != "struct PactInteractionIterator *": + msg = ( + "ptr must be a struct PactInteractionIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr def __str__(self) -> str: """ - Informal string representation of the Matching Rule Category. + Nice string representation. """ - return self.name + return "PactInteractionIterator" def __repr__(self) -> str: """ - Information-rich string representation of the Matching Rule Category. + Debugging representation. """ - return f"MatchingRuleCategory.{self.name}" + return f"PactInteractionIterator({self._ptr!r})" + def __del__(self) -> None: + """ + Destructor for the Pact Interaction Iterator. + """ + pact_interaction_iter_delete(self) -class PactSpecification(Enum): - """ - Pact Specification. + def __next__(self) -> PactInteraction: + """ + Get the next interaction from the iterator. + """ + return pact_interaction_iter_next(self) - [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/models/pact_specification/enum.PactSpecification.html) - """ - UNKNOWN = lib.PactSpecification_Unknown - V1 = lib.PactSpecification_V1 - V1_1 = lib.PactSpecification_V1_1 - V2 = lib.PactSpecification_V2 - V3 = lib.PactSpecification_V3 - V4 = lib.PactSpecification_V4 +class PactSyncHttpIterator: + """ + Iterator over a Pact's synchronous HTTP interactions. + """ - @classmethod - def from_str(cls, version: str) -> PactSpecification: + def __init__(self, ptr: cffi.FFI.CData) -> None: """ - Instantiate a Pact Specification from a string. - - This method is case-insensitive, and allows for the version to be - specified with or without a leading "V", and with either a dot or an - underscore as the separator. + Initialise a new Pact Synchronous HTTP Iterator. Args: - version: - The version of the Pact Specification. + ptr: + CFFI data structure. - Returns: - The Pact Specification. + Raises: + TypeError: + If the `ptr` is not a `struct PactSyncHttpIterator`. """ - version = version.upper().replace(".", "_") - if version.startswith("V"): - return cls[version] - return cls["V" + version] + if ffi.typeof(ptr).cname != "struct PactSyncHttpIterator *": + msg = ( + "ptr must be a struct PactSyncHttpIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr def __str__(self) -> str: """ - Informal string representation of the Pact Specification. + Nice string representation. """ - return self.name + return "PactSyncHttpIterator" def __repr__(self) -> str: """ - Information-rich string representation of the Pact Specification. + Debugging representation. """ - return f"PactSpecification.{self.name}" - + return f"PactSyncHttpIterator({self._ptr!r})" -class StringResult: - """ - String result. - """ + def __del__(self) -> None: + """ + Destructor for the Pact Synchronous HTTP Iterator. + """ + pact_sync_http_iter_delete(self) - class _StringResult(Enum): + def __iter__(self) -> Self: """ - Internal enum from Pact FFI. + Return the iterator itself. + """ + return self - [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/mock_server/enum.StringResult.html) + def __next__(self) -> SynchronousHttp: """ + Get the next message from the iterator. + """ + return pact_sync_http_iter_next(self) - FAILED = lib.StringResult_Failed - OK = lib.StringResult_Ok - class _StringResultCData: - tag: int - ok: cffi.FFI.CData - failed: cffi.FFI.CData +class PactSyncMessageIterator: + """ + Iterator over a Pact's synchronous messages. + """ - def __init__(self, cdata: cffi.FFI.CData) -> None: + def __init__(self, ptr: cffi.FFI.CData) -> None: """ - Initialise a new String Result. + Initialise a new Pact Synchronous Message Iterator. Args: - cdata: + ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct PactSyncMessageIterator`. """ - if ffi.typeof(cdata).cname != "struct StringResult": - msg = f"cdata must be a struct StringResult, got {ffi.typeof(cdata).cname}" + if ffi.typeof(ptr).cname != "struct PactSyncMessageIterator *": + msg = ( + "ptr must be a struct PactSyncMessageIterator, got" + f" {ffi.typeof(ptr).cname}" + ) raise TypeError(msg) - self._cdata = typing.cast(StringResult._StringResultCData, cdata) + self._ptr = ptr def __str__(self) -> str: """ - String representation of the String Result. + Nice string representation. """ - return self.text + return "PactSyncMessageIterator" def __repr__(self) -> str: """ - Debugging string representation of the String Result. + Debugging representation. """ - return f"" + return f"PactSyncMessageIterator({self._ptr!r})" - @property - def is_failed(self) -> bool: + def __del__(self) -> None: """ - Whether the result is an error. + Destructor for the Pact Synchronous Message Iterator. """ - return self._cdata.tag == StringResult._StringResult.FAILED.value + pact_sync_message_iter_delete(self) - @property - def is_ok(self) -> bool: + def __iter__(self) -> Self: """ - Whether the result is ok. + Return the iterator itself. """ - return self._cdata.tag == StringResult._StringResult.OK.value + return self - @property - def text(self) -> str: + def __next__(self) -> SynchronousMessage: """ - The text of the result. + Get the next message from the iterator. """ - # The specific `.ok` or `.failed` does not matter. - s = ffi.string(self._cdata.ok) - if isinstance(s, bytes): - return s.decode("utf-8") - return s + return pact_sync_message_iter_next(self) - def raise_exception(self) -> None: - """ - Raise an exception with the text of the result. - Raises: - RuntimeError: If the result is an error. +class Provider: ... + + +class ProviderState: + def __init__(self, ptr: cffi.FFI.CData) -> None: """ - if self.is_failed: - raise RuntimeError(self.text) + Initialise a new ProviderState. + Args: + ptr: + CFFI data structure. -class OwnedString(str): - """ - A string that owns its own memory. + Raises: + TypeError: + If the `ptr` is not a `struct ProviderState`. + """ + if ffi.typeof(ptr).cname != "struct ProviderState *": + msg = "ptr must be a struct ProviderState, got" f" {ffi.typeof(ptr).cname}" + raise TypeError(msg) + self._ptr = ptr - This is used to ensure that the memory is freed when the string is - destroyed. + def __str__(self) -> str: + """ + Nice string representation. + """ + return "ProviderState({self.name!r})" - As this is subclassed from `str`, it can be used in place of a normal string - in most cases. - """ + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"ProviderState({self._ptr!r})" - __slots__ = ("_ptr", "_string") + @property + def name(self) -> str: + """ + Provider State name. + """ + return provider_state_get_name(self) or "" - def __new__(cls, ptr: cffi.FFI.CData) -> Self: + def parameters(self) -> GeneratorType[Tuple[str, str], None, None]: """ - Create a new Owned String. + Provider State parameters. - As this is a subclass of the immutable type `str`, we need to override - the `__new__` method to ensure that the string is initialised correctly. + This is a generator that yields key-value pairs. """ - s = ffi.string(ptr) - return super().__new__(cls, s if isinstance(s, str) else s.decode("utf-8")) + for p in provider_state_get_param_iter(self): + yield p.key, p.value + return # Ensures that the parent object outlives the generator + + +class ProviderStateIterator: + """ + Iterator over an interactions ProviderStates. + """ def __init__(self, ptr: cffi.FFI.CData) -> None: """ - Initialise a new Owned String. + Initialise a new Provider State Iterator. Args: ptr: CFFI data structure. + + Raises: + TypeError: + If the `ptr` is not a `struct ProviderStateIterator`. """ + if ffi.typeof(ptr).cname != "struct ProviderStateIterator *": + msg = ( + "ptr must be a struct ProviderStateIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) self._ptr = ptr - s = ffi.string(ptr) - self._string = s if isinstance(s, str) else s.decode("utf-8") def __str__(self) -> str: """ - String representation of the Owned String. + Nice string representation. """ - return self._string + return "ProviderStateIterator" def __repr__(self) -> str: """ - Debugging string representation of the Owned String. + Debugging representation. """ - return f"" + return f"ProviderStateIterator({self._ptr!r})" def __del__(self) -> None: """ - Destructor for the Owned String. + Destructor for the Provider State Iterator. """ - string_delete(self) + provider_state_iter_delete(self) - def __eq__(self, other: object) -> bool: + def __iter__(self) -> ProviderStateIterator: """ - Equality comparison. - - Args: - other: - The object to compare to. - - Returns: - Whether the two objects are equal. + Return the iterator itself. """ - if isinstance(other, OwnedString): - return self._ptr == other._ptr - if isinstance(other, str): - return self._string == other - return super().__eq__(other) - + return self -def version() -> str: - """ - Return the version of the pact_ffi library. + def __next__(self) -> ProviderState: + """ + Get the next message from the iterator. + """ + return provider_state_iter_next(self) - [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_version) - Returns: - The version of the pact_ffi library as a string, in the form of `x.y.z`. +class ProviderStateParamIterator: """ - v = ffi.string(lib.pactffi_version()) - if isinstance(v, bytes): - return v.decode("utf-8") - return v - - -def init(log_env_var: str) -> None: + Iterator over a Provider States Parameters. """ - Initialise the mock server library. - This can provide an environment variable name to use to set the log levels. - This function should only be called once, as it tries to install a global - tracing subscriber. + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Provider State Param Iterator. - [Rust - `pactffi_init`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_init) + Args: + ptr: + CFFI data structure. - # Safety + Raises: + TypeError: + If the `ptr` is not a `struct ProviderStateParamIterator`. + """ + if ffi.typeof(ptr).cname != "struct ProviderStateParamIterator *": + msg = ( + "ptr must be a struct ProviderStateParamIterator, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr - log_env_var must be a valid NULL terminated UTF-8 string. - """ - raise NotImplementedError + def __str__(self) -> str: + """ + Nice string representation. + """ + return "ProviderStateParamIterator" + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"ProviderStateParamIterator({self._ptr!r})" -def init_with_log_level(level: str = "INFO") -> None: - """ - Initialises logging, and sets the log level explicitly. + def __del__(self) -> None: + """ + Destructor for the Provider State Param Iterator. + """ + provider_state_param_iter_delete(self) - This function should only be called once, as it tries to install a global - tracing subscriber. + def __iter__(self) -> ProviderStateParamIterator: + """ + Return the iterator itself. + """ + return self - [Rust - `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_init_with_log_level) + def __next__(self) -> ProviderStateParamPair: + """ + Get the next message from the iterator. + """ + return provider_state_param_iter_next(self) - Args: - level: - One of TRACE, DEBUG, INFO, WARN, ERROR, NONE/OFF. Case-insensitive. - # Safety +class ProviderStateParamPair: + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new ProviderStateParamPair. - Exported functions are inherently unsafe. - """ - raise NotImplementedError + Args: + ptr: + CFFI data structure. + Raises: + TypeError: + If the `ptr` is not a `struct ProviderStateParamPair`. + """ + if ffi.typeof(ptr).cname != "struct ProviderStateParamPair *": + msg = ( + "ptr must be a struct ProviderStateParamPair, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr -def enable_ansi_support() -> None: - """ - Enable ANSI coloured output on Windows. + def __str__(self) -> str: + """ + Nice string representation. + """ + return "ProviderStateParamPair" - On non-Windows platforms, this function is a no-op. + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"ProviderStateParamPair({self._ptr!r})" - [Rust - `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_enable_ansi_support) + def __del__(self) -> None: + """ + Destructor for the Provider State Param Pair. + """ + provider_state_param_pair_delete(self) - # Safety + @property + def key(self) -> str: + """ + Provider State Param key. + """ + s = ffi.string(self._ptr.key) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s - This function is safe. - """ - raise NotImplementedError + @property + def value(self) -> str: + """ + Provider State Param value. + """ + s = ffi.string(self._ptr.value) # type: ignore[attr-defined] + if isinstance(s, bytes): + s = s.decode("utf-8") + return s -def log_message( - message: str, - log_level: LevelFilter | str = LevelFilter.ERROR, - source: str | None = None, -) -> None: - """ - Log using the shared core logging facility. +class SynchronousHttp: + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: + """ + Initialise a new Synchronous HTTP Interaction. - [Rust - `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_message) + Args: + ptr: + CFFI data structure. - This is useful for callers to have a single set of logs. + owned: + Whether the message is owned by something else or not. This + determines whether the message should be freed when the Python + object is destroyed. - Args: - message: - The contents written to the log + Raises: + TypeError: + If the `ptr` is not a `struct SynchronousHttp`. + """ + if ffi.typeof(ptr).cname != "struct SynchronousHttp *": + msg = ( + "ptr must be a struct SynchronousHttp, got" f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + self._owned = owned - log_level: - The verbosity at which this message should be logged. + def __str__(self) -> str: + """ + Nice string representation. + """ + return "SynchronousHttp" - source: - The source of the log, such as the class, module or caller. - """ - if isinstance(log_level, str): - log_level = LevelFilter[log_level.upper()] - if source is None: - import inspect + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"SynchronousHttp({self._ptr!r})" - source = inspect.stack()[1].function - lib.pactffi_log_message( - source.encode("utf-8"), - log_level.name.encode("utf-8"), - message.encode("utf-8"), - ) + def __del__(self) -> None: + """ + Destructor for the SynchronousHttp. + """ + if not self._owned: + sync_http_delete(self) + @property + def description(self) -> str: + """ + Description of this message interaction. -def match_message(msg_1: Message, msg_2: Message) -> Mismatches: - """ - Match a pair of messages, producing a collection of mismatches. + This needs to be unique in the pact file. + """ + return sync_http_get_description(self) - If the messages match, the returned collection will be empty. + def provider_states(self) -> GeneratorType[ProviderState, None, None]: + """ + Optional provider state for the interaction. + """ + yield from sync_http_get_provider_state_iter(self) + return # Ensures that the parent object outlives the generator - [Rust - `pactffi_match_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_match_message) - """ - raise NotImplementedError + @property + def request_contents(self) -> str | bytes | None: + """ + The contents of the request. + """ + return sync_http_get_request_contents( + self + ) or sync_http_get_request_contents_bin(self) + @property + def response_contents(self) -> str | bytes | None: + """ + The contents of the response. + """ + return sync_http_get_response_contents( + self + ) or sync_http_get_response_contents_bin(self) -def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: - """ - Get an iterator over mismatches. - [Rust - `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatches_get_iter) - """ - raise NotImplementedError +class SynchronousMessage: + def __init__(self, ptr: cffi.FFI.CData, *, owned: bool = False) -> None: + """ + Initialise a new Synchronous Message. + Args: + ptr: + CFFI data structure. -def mismatches_delete(mismatches: Mismatches) -> None: - """ - Delete mismatches. + owned: + Whether the message is owned by something else or not. This + determines whether the message should be freed when the Python + object is destroyed. - [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatches_delete) - """ - raise NotImplementedError + Raises: + TypeError: + If the `ptr` is not a `struct SynchronousMessage`. + """ + if ffi.typeof(ptr).cname != "struct SynchronousMessage *": + msg = ( + "ptr must be a struct SynchronousMessage, got" + f" {ffi.typeof(ptr).cname}" + ) + raise TypeError(msg) + self._ptr = ptr + self._owned = owned + def __str__(self) -> str: + """ + Nice string representation. + """ + return "SynchronousMessage" -def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch: - """ - Get the next mismatch from a mismatches iterator. + def __repr__(self) -> str: + """ + Debugging representation. + """ + return f"SynchronousMessage({self._ptr!r})" - [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatches_iter_next) + def __del__(self) -> None: + """ + Destructor for the SynchronousMessage. + """ + if not self._owned: + sync_message_delete(self) - Returns a null pointer if no mismatches remain. - """ - raise NotImplementedError + @property + def description(self) -> str: + """ + Description of this message interaction. + This needs to be unique in the pact file. + """ + return sync_message_get_description(self) -def mismatches_iter_delete(iter: MismatchesIterator) -> None: - """ - Delete a mismatches iterator when you're done with it. + def provider_states(self) -> GeneratorType[ProviderState, None, None]: + """ + Optional provider state for the interaction. + """ + yield from sync_message_get_provider_state_iter(self) + return # Ensures that the parent object outlives the generator - [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatches_iter_delete) - """ - raise NotImplementedError + @property + def request_contents(self) -> MessageContents: + """ + The contents of the message. + """ + return sync_message_generate_request_contents(self) + @property + def response_contents(self) -> GeneratorType[MessageContents, None, None]: + """ + The contents of the responses. + """ + yield from ( + sync_message_generate_response_contents(self, i) + for i in range(sync_message_get_number_responses(self)) + ) + return # Ensures that the parent object outlives the generator -def mismatch_to_json(mismatch: Mismatch) -> str: - """ - Get a JSON representation of the mismatch. - [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_to_json) +class VerifierHandle: """ - raise NotImplementedError - + Handle to a Verifier. -def mismatch_type(mismatch: Mismatch) -> str: + [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/verifier/handle/struct.VerifierHandle.html) """ - Get the type of a mismatch. - [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_type) - """ - raise NotImplementedError + def __init__(self, ref: cffi.FFI.CData) -> None: + """ + Initialise a new Verifier Handle. + Args: + ref: + Rust library reference to the Verifier Handle. + """ + self._ref = ref -def mismatch_summary(mismatch: Mismatch) -> str: - """ - Get a summary of a mismatch. + def __del__(self) -> None: + """ + Destructor for the Verifier Handle. + """ + verifier_shutdown(self) - [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_summary) - """ - raise NotImplementedError - - -def mismatch_description(mismatch: Mismatch) -> str: - """ - Get a description of a mismatch. - - [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_description) - """ - raise NotImplementedError - - -def mismatch_ansi_description(mismatch: Mismatch) -> str: - """ - Get an ANSI-compatible description of a mismatch. - - [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mismatch_ansi_description) - """ - raise NotImplementedError - - -def get_error_message(length: int = 1024) -> str | None: - """ - Provide the error message from `LAST_ERROR` to the calling C code. - - [Rust - `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_get_error_message) - - This function should be called after any other function in the pact_matching - FFI indicates a failure with its own error message, if the caller wants to - get more context on why the error happened. - - Do note that this error-reporting mechanism only reports the top-level error - message, not any source information embedded in the original Rust error - type. If you want more detailed information for debugging purposes, use the - logging interface. - - Args: - length: - The length of the buffer to allocate for the error message. If the - error message is longer than this, it will be truncated. - - Returns: - A string containing the error message, or None if there is no error - message. - - Raises: - RuntimeError: If the error message could not be retrieved. - """ - buffer = ffi.new("char[]", length) - ret: int = lib.pactffi_get_error_message(buffer, length) - - if ret >= 0: - # While the documentation says that the return value is the number of bytes - # written, the actually return value is always 0 on success. - if msg := ffi.string(buffer): - if isinstance(msg, bytes): - return msg.decode("utf-8") - return msg - return None - if ret == -1: - msg = "The provided buffer is a null pointer." - elif ret == -2: # noqa: PLR2004 - # Instead of returning an error here, we call the function again with a - # larger buffer. - return get_error_message(length * 32) - elif ret == -3: # noqa: PLR2004 - msg = "The write failed for some other reason." - elif ret == -4: # noqa: PLR2004 - msg = "The error message had an interior NULL." - else: - msg = "An unknown error occurred." - raise RuntimeError(msg) - - -def log_to_stdout(level_filter: LevelFilter) -> int: - """ - Convenience function to direct all logging to stdout. - - [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_to_stdout) - """ - raise NotImplementedError - - -def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: - """ - Convenience function to direct all logging to stderr. - - [Rust - `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_to_stderr) - - Args: - level_filter: - The level of logs to filter to. If a string is given, it must match - one of the [`LevelFilter`][pact.v3.ffi.LevelFilter] values (case - insensitive). - - Raises: - RuntimeError: If there was an error setting the logger. - """ - if isinstance(level_filter, str): - level_filter = LevelFilter[level_filter.upper()] - ret: int = lib.pactffi_log_to_stderr(level_filter.value) - if ret != 0: - msg = "There was an unknown error setting the logger." - raise RuntimeError(msg) - - -def log_to_file(file_name: str, level_filter: LevelFilter) -> int: - """ - Convenience function to direct all logging to a file. - - [Rust - `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_to_file) - - # Safety - - This function will fail if the file_name pointer is invalid or does not - point to a NULL terminated string. - """ - raise NotImplementedError - - -def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: - """ - Convenience function to direct all logging to a task local memory buffer. - - [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_log_to_buffer) - """ - if isinstance(level_filter, str): - level_filter = LevelFilter[level_filter.upper()] - ret: int = lib.pactffi_log_to_buffer(level_filter.value) - if ret != 0: - msg = "There was an unknown error setting the logger." - raise RuntimeError(msg) - - -def logger_init() -> None: - """ - Initialize the FFI logger with no sinks. - - [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_logger_init) - - This initialized logger does nothing until `pactffi_logger_apply` has been called. - - # Usage - - ```c - pactffi_logger_init(); - ``` - - # Safety - - This function is always safe to call. - """ - raise NotImplementedError - - -def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: - """ - Attach an additional sink to the thread-local logger. - - [Rust - `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_logger_attach_sink) - - This logger does nothing until `pactffi_logger_apply` has been called. - - Types of sinks can be specified: - - - stdout (`pactffi_logger_attach_sink("stdout", LevelFilter_Info)`) - - stderr (`pactffi_logger_attach_sink("stderr", LevelFilter_Debug)`) - - file w/ file path (`pactffi_logger_attach_sink("file /some/file/path", - LevelFilter_Trace)`) - - buffer (`pactffi_logger_attach_sink("buffer", LevelFilter_Debug)`) - - # Usage - - ```c - int result = pactffi_logger_attach_sink("file /some/file/path", LogLevel_Filter); - ``` - - # Error Handling - - The return error codes are as follows: - - - `-1`: Can't set logger (applying the logger failed, perhaps because one is - applied already). - - `-2`: No logger has been initialized (call `pactffi_logger_init` before - any other log function). - - `-3`: The sink specifier was not UTF-8 encoded. - - `-4`: The sink type specified is not a known type (known types: "stdout", - "stderr", or "file /some/path"). - - `-5`: No file path was specified in a file-type sink specification. - - `-6`: Opening a sink to the specified file path failed (check - permissions). - - # Safety - - This function checks the validity of the passed-in sink specifier, and - errors out if the specifier isn't valid UTF-8. Passing in an invalid or NULL - pointer will result in undefined behaviour. - """ - raise NotImplementedError - - -def logger_apply() -> int: - """ - Apply the previously configured sinks and levels to the program. - - [Rust - `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_logger_apply) - - If no sinks have been setup, will set the log level to info and the target - to standard out. - - This function will install a global tracing subscriber. Any attempts to - modify the logger after the call to `logger_apply` will fail. - """ - raise NotImplementedError - - -def fetch_log_buffer(log_id: str) -> str: - """ - Fetch the in-memory logger buffer contents. - - [Rust - `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_fetch_log_buffer) - - This will only have any contents if the `buffer` sink has been configured to - log to. The contents will be allocated on the heap and will need to be freed - with `pactffi_string_delete`. - - Fetches the logs associated with the provided identifier, or uses the - "global" one if the identifier is not specified (i.e. NULL). - - Returns a NULL pointer if the buffer can't be fetched. This can occur is - there is not sufficient memory to make a copy of the contents or the buffer - contains non-UTF-8 characters. - - # Safety - - This function will fail if the log_id pointer is invalid or does not point - to a NULL terminated string. - """ - raise NotImplementedError - - -def parse_pact_json(json: str) -> Pact: - """ - Parses the provided JSON into a Pact model. - - [Rust - `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_parse_pact_json) - - The returned Pact model must be freed with the `pactffi_pact_model_delete` - function when no longer needed. - - # Error Handling - - This function will return a NULL pointer if passed a NULL pointer or if an - error occurs. - """ - raise NotImplementedError - - -def pact_model_delete(pact: Pact) -> None: - """ - Frees the memory used by the Pact model. - - [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_model_delete) - """ - raise NotImplementedError - - -def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: - """ - Returns an iterator over all the interactions in the Pact. - - [Rust - `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_model_interaction_iterator) - - The iterator will have to be deleted using the - `pactffi_pact_interaction_iter_delete` function. The iterator will contain a - copy of the interactions, so it will not be affected but mutations to the - Pact model and will still function if the Pact model is deleted. - - # Safety This function is safe as long as the Pact pointer is a valid - pointer. - - # Errors On any error, this function will return a NULL pointer. - """ - raise NotImplementedError - - -def pact_spec_version(pact: Pact) -> PactSpecification: - """ - Returns the Pact specification enum that the Pact is for. - - [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_spec_version) - """ - raise NotImplementedError - - -def pact_interaction_delete(interaction: PactInteraction) -> None: - """ - Frees the memory used by the Pact interaction model. - - [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_delete) - """ - raise NotImplementedError - - -def async_message_new() -> AsynchronousMessage: - """ - Get a mutable pointer to a newly-created default message on the heap. - - [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_new) - - # Safety - - This function is safe. - - # Error Handling - - Returns NULL on error. - """ - raise NotImplementedError - - -def async_message_delete(message: AsynchronousMessage) -> None: - """ - Destroy the `AsynchronousMessage` being pointed to. - - [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_delete) - """ - raise NotImplementedError - - -def async_message_get_contents(message: AsynchronousMessage) -> MessageContents: - """ - Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. - - [Rust - `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_contents) - - # Safety + def __str__(self) -> str: + """ + String representation of the Verifier Handle. + """ + return f"VerifierHandle({hex(id(self._ref))})" - The data pointed to by the pointer this function returns will be deleted - when the message is deleted. Trying to use if after the message is deleted - will result in undefined behaviour. + def __repr__(self) -> str: + """ + String representation of the Verifier Handle. + """ + return f"" - # Error Handling - If the message is NULL, returns NULL. +class ExpressionValueType(Enum): """ - raise NotImplementedError - + Expression Value Type. -def async_message_get_contents_str(message: AsynchronousMessage) -> str: + [Rust `ExpressionValueType`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/expressions/enum.ExpressionValueType.html) """ - Get the message contents of an `AsynchronousMessage` in string form. - - [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_contents_str) - # Safety + UNKNOWN = lib.ExpressionValueType_Unknown + STRING = lib.ExpressionValueType_String + NUMBER = lib.ExpressionValueType_Number + INTEGER = lib.ExpressionValueType_Integer + DECIMAL = lib.ExpressionValueType_Decimal + BOOLEAN = lib.ExpressionValueType_Boolean - The returned string must be deleted with `pactffi_string_delete`. + def __str__(self) -> str: + """ + Informal string representation of the Expression Value Type. + """ + return self.name - The returned string can outlive the message. + def __repr__(self) -> str: + """ + Information-rich string representation of the Expression Value Type. + """ + return f"ExpressionValueType.{self.name}" - # Error Handling - If the message is NULL, returns NULL. If the body of the message - is missing, then this function also returns NULL. This means there's - no mechanism to differentiate with this function call alone between - a NULL message and a missing message body. +class GeneratorCategory(Enum): """ - raise NotImplementedError - + Generator Category. -def async_message_set_contents_str( - message: AsynchronousMessage, - contents: str, - content_type: str, -) -> None: + [Rust `GeneratorCategory`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/generators/enum.GeneratorCategory.html) """ - Sets the contents of the message as a string. - - [Rust - `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_set_contents_str) - - `message` - the message to set the contents for - - `contents` - pointer to contents to copy from. Must be a valid - NULL-terminated UTF-8 string pointer. - - `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. + METHOD = lib.GeneratorCategory_METHOD + PATH = lib.GeneratorCategory_PATH + HEADER = lib.GeneratorCategory_HEADER + QUERY = lib.GeneratorCategory_QUERY + BODY = lib.GeneratorCategory_BODY + STATUS = lib.GeneratorCategory_STATUS + METADATA = lib.GeneratorCategory_METADATA - # Safety + def __str__(self) -> str: + """ + Informal string representation of the Generator Category. + """ + return self.name - The message contents and content type must either be NULL pointers, or point - to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is - undefined. + def __repr__(self) -> str: + """ + Information-rich string representation of the Generator Category. + """ + return f"GeneratorCategory.{self.name}" - # Error Handling - If the contents is a NULL pointer, it will set the message contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. +class InteractionPart(Enum): """ - raise NotImplementedError - + Interaction Part. -def async_message_get_contents_length(message: AsynchronousMessage) -> int: + [Rust `InteractionPart`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/handles/enum.InteractionPart.html) """ - Get the length of the contents of a `AsynchronousMessage`. - [Rust - `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_contents_length) - - # Safety - - This function is safe. + REQUEST = lib.InteractionPart_Request + RESPONSE = lib.InteractionPart_Response - # Error Handling + def __str__(self) -> str: + """ + Informal string representation of the Interaction Part. + """ + return self.name - If the message is NULL, returns 0. If the body of the request is missing, - then this function also returns 0. - """ - raise NotImplementedError + def __repr__(self) -> str: + """ + Information-rich string representation of the Interaction Part. + """ + return f"InteractionPath.{self.name}" -def async_message_get_contents_bin(message: AsynchronousMessage) -> str: - """ - Get the contents of an `AsynchronousMessage` as bytes. +class LevelFilter(Enum): + """Level Filter.""" - [Rust - `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_contents_bin) + OFF = lib.LevelFilter_Off + ERROR = lib.LevelFilter_Error + WARN = lib.LevelFilter_Warn + INFO = lib.LevelFilter_Info + DEBUG = lib.LevelFilter_Debug + TRACE = lib.LevelFilter_Trace - # Safety + def __str__(self) -> str: + """ + Informal string representation of the Level Filter. + """ + return self.name - The number of bytes in the buffer will be returned by - `pactffi_async_message_get_contents_length`. It is safe to use the pointer - while the message is not deleted or changed. Using the pointer after the - message is mutated or deleted may lead to undefined behaviour. + def __repr__(self) -> str: + """ + Information-rich string representation of the Level Filter. + """ + return f"LevelFilter.{self.name}" - # Error Handling - If the message is NULL, returns NULL. If the body of the message is missing, - then this function also returns NULL. +class MatchingRuleCategory(Enum): """ - raise NotImplementedError - + Matching Rule Category. -def async_message_set_contents_bin( - message: AsynchronousMessage, - contents: str, - len: int, - content_type: str, -) -> None: + [Rust `MatchingRuleCategory`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/matching_rules/enum.MatchingRuleCategory.html) """ - Sets the contents of the message as an array of bytes. - [Rust - `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_set_contents_bin) + METHOD = lib.MatchingRuleCategory_METHOD + PATH = lib.MatchingRuleCategory_PATH + HEADER = lib.MatchingRuleCategory_HEADER + QUERY = lib.MatchingRuleCategory_QUERY + BODY = lib.MatchingRuleCategory_BODY + STATUS = lib.MatchingRuleCategory_STATUS + CONTENST = lib.MatchingRuleCategory_CONTENTS + METADATA = lib.MatchingRuleCategory_METADATA - * `message` - the message to set the contents for - * `contents` - pointer to contents to copy from - * `len` - number of bytes to copy from the contents pointer - * `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. + def __str__(self) -> str: + """ + Informal string representation of the Matching Rule Category. + """ + return self.name - # Safety + def __repr__(self) -> str: + """ + Information-rich string representation of the Matching Rule Category. + """ + return f"MatchingRuleCategory.{self.name}" - The contents pointer must be valid for reads of `len` bytes, and it must be - properly aligned and consecutive. Otherwise behaviour is undefined. - # Error Handling +class PactSpecification(Enum): + """ + Pact Specification. - If the contents is a NULL pointer, it will set the message contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. + [Rust `PactSpecification`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/models/pact_specification/enum.PactSpecification.html) """ - raise NotImplementedError + UNKNOWN = lib.PactSpecification_Unknown + V1 = lib.PactSpecification_V1 + V1_1 = lib.PactSpecification_V1_1 + V2 = lib.PactSpecification_V2 + V3 = lib.PactSpecification_V3 + V4 = lib.PactSpecification_V4 -def async_message_get_description(message: AsynchronousMessage) -> str: - r""" - Get a copy of the description. + @classmethod + def from_str(cls, version: str) -> PactSpecification: + """ + Instantiate a Pact Specification from a string. - [Rust - `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_description) + This method is case-insensitive, and allows for the version to be + specified with or without a leading "V", and with either a dot or an + underscore as the separator. - # Safety + Args: + version: + The version of the Pact Specification. - The returned string must be deleted with `pactffi_string_delete`. + Returns: + The Pact Specification. + """ + version = version.upper().replace(".", "_") + if version.startswith("V"): + return cls[version] + return cls["V" + version] - Since it is a copy, the returned string may safely outlive the - `AsynchronousMessage`. + def __str__(self) -> str: + """ + Informal string representation of the Pact Specification. + """ + return self.name - # Errors + def __repr__(self) -> str: + """ + Information-rich string representation of the Pact Specification. + """ + return f"PactSpecification.{self.name}" - On failure, this function will return a NULL pointer. - This function may fail if the Rust string contains embedded null ('\0') - bytes. +class StringResult: + """ + String result. """ - raise NotImplementedError + class _StringResult(Enum): + """ + Internal enum from Pact FFI. -def async_message_set_description( - message: AsynchronousMessage, - description: str, -) -> int: - """ - Write the `description` field on the `AsynchronousMessage`. + [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/mock_server/enum.StringResult.html) + """ - [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_set_description) + FAILED = lib.StringResult_Failed + OK = lib.StringResult_Ok - # Safety + class _StringResultCData: + tag: int + ok: cffi.FFI.CData + failed: cffi.FFI.CData - `description` must contain valid UTF-8. Invalid UTF-8 - will be replaced with U+FFFD REPLACEMENT CHARACTER. + def __init__(self, cdata: cffi.FFI.CData) -> None: + """ + Initialise a new String Result. - This function will only reallocate if the new string - does not fit in the existing buffer. + Args: + cdata: + CFFI data structure. - # Error Handling + Raises: + TypeError: + If the `cdata` is not a `struct StringResult`. + """ + if ffi.typeof(cdata).cname != "struct StringResult": + msg = f"cdata must be a struct StringResult, got {ffi.typeof(cdata).cname}" + raise TypeError(msg) + self._cdata = typing.cast(StringResult._StringResultCData, cdata) - Errors will be reported with a non-zero return value. - """ - raise NotImplementedError + def __str__(self) -> str: + """ + String representation of the String Result. + """ + return self.text + def __repr__(self) -> str: + """ + Debugging string representation of the String Result. + """ + return f"" -def async_message_get_provider_state( - message: AsynchronousMessage, - index: int, -) -> ProviderState: - r""" - Get a copy of the provider state at the given index from this message. + @property + def is_failed(self) -> bool: + """ + Whether the result is an error. + """ + return self._cdata.tag == StringResult._StringResult.FAILED.value - [Rust - `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_provider_state) + @property + def is_ok(self) -> bool: + """ + Whether the result is ok. + """ + return self._cdata.tag == StringResult._StringResult.OK.value - # Safety + @property + def text(self) -> str: + """ + The text of the result. + """ + # The specific `.ok` or `.failed` does not matter. + s = ffi.string(self._cdata.ok) + if isinstance(s, bytes): + return s.decode("utf-8") + return s - The returned structure must be deleted with `provider_state_delete`. + def raise_exception(self) -> None: + """ + Raise an exception with the text of the result. - Since it is a copy, the returned structure may safely outlive the - `AsynchronousMessage`. + Raises: + RuntimeError: + If the result is an error. - # Error Handling + Raises: + RuntimeError: + If the result is an error. + """ + if self.is_failed: + raise RuntimeError(self.text) - On failure, this function will return a variant other than Success. - This function may fail if the index requested is out of bounds, or if any of - the Rust strings contain embedded null ('\0') bytes. +class OwnedString(str): """ - raise NotImplementedError + A string that owns its own memory. + This is used to ensure that the memory is freed when the string is + destroyed. -def async_message_get_provider_state_iter( - message: AsynchronousMessage, -) -> ProviderStateIterator: + As this is subclassed from `str`, it can be used in place of a normal string + in most cases. """ - Get an iterator over provider states. - - [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) - - # Safety - The underlying data must not change during iteration. - - # Error Handling + __slots__ = ("_ptr", "_string") - Returns NULL if an error occurs. - """ - raise NotImplementedError + def __new__(cls, ptr: cffi.FFI.CData) -> Self: + """ + Create a new Owned String. + As this is a subclass of the immutable type `str`, we need to override + the `__new__` method to ensure that the string is initialised correctly. + """ + s = ffi.string(ptr) + return super().__new__(cls, s if isinstance(s, str) else s.decode("utf-8")) -def consumer_get_name(consumer: Consumer) -> str: - r""" - Get a copy of this consumer's name. + def __init__(self, ptr: cffi.FFI.CData) -> None: + """ + Initialise a new Owned String. - [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_consumer_get_name) + Args: + ptr: + CFFI data structure. + """ + self._ptr = ptr + s = ffi.string(ptr) + self._string = s if isinstance(s, str) else s.decode("utf-8") - The copy must be deleted with `pactffi_string_delete`. + def __str__(self) -> str: + """ + String representation of the Owned String. + """ + return self._string - # Usage + def __repr__(self) -> str: + """ + Debugging string representation of the Owned String. + """ + return f"" - ```c - // Assuming `file_name` and `json_str` are already defined. + def __del__(self) -> None: + """ + Destructor for the Owned String. + """ + string_delete(self) - MessagePact *message_pact = pactffi_message_pact_new_from_json(file_name, json_str); - if (message_pact == NULLPTR) { - // handle error. - } + def __eq__(self, other: object) -> bool: + """ + Equality comparison. - Consumer *consumer = pactffi_message_pact_get_consumer(message_pact); - if (consumer == NULLPTR) { - // handle error. - } + Args: + other: + The object to compare to. - char *name = pactffi_consumer_get_name(consumer); - if (name == NULL) { - // handle error. - } + Returns: + Whether the two objects are equal. + """ + if isinstance(other, OwnedString): + return self._ptr == other._ptr + if isinstance(other, str): + return self._string == other + return super().__eq__(other) - printf("%s\n", name); - pactffi_string_delete(name); - ``` +def version() -> str: + """ + Return the version of the pact_ffi library. - # Errors + [Rust `pactffi_version`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_version) - This function will fail if it is passed a NULL pointer, - or the Rust string contains an embedded NULL byte. - In the case of error, a NULL pointer will be returned. + Returns: + The version of the pact_ffi library as a string, in the form of `x.y.z`. """ - raise NotImplementedError + v = ffi.string(lib.pactffi_version()) + if isinstance(v, bytes): + return v.decode("utf-8") + return v -def pact_get_consumer(pact: Pact) -> Consumer: +def init(log_env_var: str) -> None: """ - Get the consumer from a Pact. + Initialise the mock server library. - This returns a copy of the consumer model, and needs to be cleaned up with - `pactffi_pact_consumer_delete` when no longer required. + This can provide an environment variable name to use to set the log levels. + This function should only be called once, as it tries to install a global + tracing subscriber. [Rust - `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_get_consumer) + `pactffi_init`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_init) - # Errors + # Safety - This function will fail if it is passed a NULL pointer. In the case of - error, a NULL pointer will be returned. + log_env_var must be a valid NULL terminated UTF-8 string. """ raise NotImplementedError -def pact_consumer_delete(consumer: Consumer) -> None: - """ - Frees the memory used by the Pact consumer. - - [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_consumer_delete) +def init_with_log_level(level: str = "INFO") -> None: """ - raise NotImplementedError + Initialises logging, and sets the log level explicitly. + This function should only be called once, as it tries to install a global + tracing subscriber. -def message_contents_get_contents_str(contents: MessageContents) -> str: - """ - Get the message contents in string form. + [Rust + `pactffi_init_with_log_level`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_init_with_log_level) - [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_contents_str) + Args: + level: + One of TRACE, DEBUG, INFO, WARN, ERROR, NONE/OFF. Case-insensitive. # Safety - The returned string must be deleted with `pactffi_string_delete`. - - The returned string can outlive the message. - - # Error Handling - - If the message contents is NULL, returns NULL. If the body of the message - is missing, then this function also returns NULL. This means there's - no mechanism to differentiate with this function call alone between - a NULL message and a missing message body. + Exported functions are inherently unsafe. """ raise NotImplementedError -def message_contents_set_contents_str( - contents: MessageContents, - contents_str: str, - content_type: str, -) -> None: +def enable_ansi_support() -> None: """ - Sets the contents of the message as a string. + Enable ANSI coloured output on Windows. - [Rust - `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_set_contents_str) + On non-Windows platforms, this function is a no-op. - * `contents` - the message contents to set the contents for - * `contents_str` - pointer to contents to copy from. Must be a valid - NULL-terminated UTF-8 string pointer. - * `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. + [Rust + `pactffi_enable_ansi_support`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_enable_ansi_support) # Safety - The message contents and content type must either be NULL pointers, or point - to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is - undefined. - - # Error Handling - - If the contents string is a NULL pointer, it will set the message contents - as null. If the content type is a null pointer, or can't be parsed, it will - set the content type as unknown. + This function is safe. """ raise NotImplementedError -def message_contents_get_contents_length(contents: MessageContents) -> int: +def log_message( + message: str, + log_level: LevelFilter | str = LevelFilter.ERROR, + source: str | None = None, +) -> None: """ - Get the length of the message contents. + Log using the shared core logging facility. - [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_contents_length) + [Rust + `pactffi_log_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_message) - # Safety + This is useful for callers to have a single set of logs. - This function is safe. + Args: + message: + The contents written to the log - # Error Handling + log_level: + The verbosity at which this message should be logged. - If the message is NULL, returns 0. If the body of the message - is missing, then this function also returns 0. + source: + The source of the log, such as the class, module or caller. """ - raise NotImplementedError + if isinstance(log_level, str): + log_level = LevelFilter[log_level.upper()] + if source is None: + import inspect + + source = inspect.stack()[1].function + lib.pactffi_log_message( + source.encode("utf-8"), + log_level.name.encode("utf-8"), + message.encode("utf-8"), + ) -def message_contents_get_contents_bin(contents: MessageContents) -> str: +def mismatches_get_iter(mismatches: Mismatches) -> MismatchesIterator: """ - Get the contents of a message as a pointer to an array of bytes. + Get an iterator over mismatches. [Rust - `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_contents_bin) + `pactffi_mismatches_get_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_get_iter) + """ + raise NotImplementedError - # Safety - The number of bytes in the buffer will be returned by - `pactffi_message_contents_get_contents_length`. It is safe to use the - pointer while the message is not deleted or changed. Using the pointer after - the message is mutated or deleted may lead to undefined behaviour. +def mismatches_delete(mismatches: Mismatches) -> None: + """ + Delete mismatches. - # Error Handling + [Rust `pactffi_mismatches_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_delete) + """ + raise NotImplementedError - If the message is NULL, returns NULL. If the body of the message is missing, - then this function also returns NULL. + +def mismatches_iter_next(iter: MismatchesIterator) -> Mismatch: + """ + Get the next mismatch from a mismatches iterator. + + [Rust `pactffi_mismatches_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_iter_next) + + Returns a null pointer if no mismatches remain. """ raise NotImplementedError -def message_contents_set_contents_bin( - contents: MessageContents, - contents_bin: str, - len: int, - content_type: str, -) -> None: +def mismatches_iter_delete(iter: MismatchesIterator) -> None: """ - Sets the contents of the message as an array of bytes. + Delete a mismatches iterator when you're done with it. - [Rust - `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_set_contents_bin) + [Rust `pactffi_mismatches_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatches_iter_delete) + """ + raise NotImplementedError - * `message` - the message contents to set the contents for - * `contents_bin` - pointer to contents to copy from - * `len` - number of bytes to copy from the contents pointer - * `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. - # Safety +def mismatch_to_json(mismatch: Mismatch) -> str: + """ + Get a JSON representation of the mismatch. - The contents pointer must be valid for reads of `len` bytes, and it must be - properly aligned and consecutive. Otherwise behaviour is undefined. + [Rust `pactffi_mismatch_to_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_to_json) + """ + raise NotImplementedError - # Error Handling - If the contents is a NULL pointer, it will set the message contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. +def mismatch_type(mismatch: Mismatch) -> str: """ - raise NotImplementedError + Get the type of a mismatch. + [Rust `pactffi_mismatch_type`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_type) + """ + raise NotImplementedError -def message_contents_get_metadata_iter( - contents: MessageContents, -) -> MessageMetadataIterator: - r""" - Get an iterator over the metadata of a message. - [Rust - `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) +def mismatch_summary(mismatch: Mismatch) -> str: + """ + Get a summary of a mismatch. - The returned pointer must be deleted with - `pactffi_message_metadata_iter_delete` when done with it. + [Rust `pactffi_mismatch_summary`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_summary) + """ + raise NotImplementedError - # Safety - This iterator carries a pointer to the message contents, and must not - outlive the message. +def mismatch_description(mismatch: Mismatch) -> str: + """ + Get a description of a mismatch. - The message metadata also must not be modified during iteration. If it is, - the old iterator must be deleted and a new iterator created. + [Rust `pactffi_mismatch_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_description) + """ + raise NotImplementedError - # Error Handling - On failure, this function will return a NULL pointer. +def mismatch_ansi_description(mismatch: Mismatch) -> str: + """ + Get an ANSI-compatible description of a mismatch. - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. + [Rust `pactffi_mismatch_ansi_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mismatch_ansi_description) """ raise NotImplementedError -def message_contents_get_matching_rule_iter( - contents: MessageContents, - category: MatchingRuleCategory, -) -> MatchingRuleCategoryIterator: - r""" - Get an iterator over the matching rules for a category of a message. +def get_error_message(length: int = 1024) -> str | None: + """ + Provide the error message from `LAST_ERROR` to the calling C code. [Rust - `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) - - The returned pointer must be deleted with - `pactffi_matching_rules_iter_delete` when done with it. + `pactffi_get_error_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_get_error_message) - Note that there could be multiple matching rules for the same key, so this - iterator will sequentially return each rule with the same key. + This function should be called after any other function in the pact_matching + FFI indicates a failure with its own error message, if the caller wants to + get more context on why the error happened. - For sample, given the following rules: + Do note that this error-reporting mechanism only reports the top-level error + message, not any source information embedded in the original Rust error + type. If you want more detailed information for debugging purposes, use the + logging interface. - ``` - "$.a" => Type, - "$.b" => Regex("\\d+"), Number - ``` + Args: + length: + The length of the buffer to allocate for the error message. If the + error message is longer than this, it will be truncated. - This iterator will return a sequence of 3 values: + Returns: + A string containing the error message, or None if there is no error + message. - - `("$.a", Type)` - - `("$.b", Regex("\d+"))` - - `("$.b", Number)` + Raises: + RuntimeError: + If the error message could not be retrieved. + """ + buffer = ffi.new("char[]", length) + ret: int = lib.pactffi_get_error_message(buffer, length) - # Safety + if ret >= 0: + # While the documentation says that the return value is the number of bytes + # written, the actually return value is always 0 on success. + if msg := ffi.string(buffer): + if isinstance(msg, bytes): + return msg.decode("utf-8") + return msg + return None + if ret == -1: + msg = "The provided buffer is a null pointer." + elif ret == -2: # noqa: PLR2004 + # Instead of returning an error here, we call the function again with a + # larger buffer. + return get_error_message(length * 32) + elif ret == -3: # noqa: PLR2004 + msg = "The write failed for some other reason." + elif ret == -4: # noqa: PLR2004 + msg = "The error message had an interior NULL." + else: + msg = "An unknown error occurred." + raise RuntimeError(msg) - The iterator contains a copy of the data, so is safe to use when the message - or message contents has been deleted. - # Error Handling +def log_to_stdout(level_filter: LevelFilter) -> int: + """ + Convenience function to direct all logging to stdout. - On failure, this function will return a NULL pointer. + [Rust `pactffi_log_to_stdout`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_stdout) """ raise NotImplementedError -def request_contents_get_matching_rule_iter( - request: HttpRequest, - category: MatchingRuleCategory, -) -> MatchingRuleCategoryIterator: - r""" - Get an iterator over the matching rules for a category of an HTTP request. +def log_to_stderr(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: + """ + Convenience function to direct all logging to stderr. - [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) + [Rust + `pactffi_log_to_stderr`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_stderr) - The returned pointer must be deleted with - `pactffi_matching_rules_iter_delete` when done with it. + Args: + level_filter: + The level of logs to filter to. If a string is given, it must match + one of the [`LevelFilter`][pact.v3.ffi.LevelFilter] values (case + insensitive). - For sample, given the following rules: + Raises: + RuntimeError: + If there was an error setting the logger. + """ + if isinstance(level_filter, str): + level_filter = LevelFilter[level_filter.upper()] + ret: int = lib.pactffi_log_to_stderr(level_filter.value) + if ret != 0: + msg = "There was an unknown error setting the logger." + raise RuntimeError(msg) - ``` - "$.a" => Type, - "$.b" => Regex("\d+"), Number - ``` - This iterator will return a sequence of 3 values: +def log_to_file(file_name: str, level_filter: LevelFilter) -> int: + """ + Convenience function to direct all logging to a file. - - `("$.a", Type)` - - `("$.b", Regex("\d+"))` - - `("$.b", Number)` + [Rust + `pactffi_log_to_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_file) # Safety - The iterator contains a copy of the data, so is safe to use when the - interaction or request contents has been deleted. - - # Error Handling - - On failure, this function will return a NULL pointer. + This function will fail if the file_name pointer is invalid or does not + point to a NULL terminated string. """ raise NotImplementedError -def response_contents_get_matching_rule_iter( - response: HttpResponse, - category: MatchingRuleCategory, -) -> MatchingRuleCategoryIterator: - r""" - Get an iterator over the matching rules for a category of an HTTP response. +def log_to_buffer(level_filter: LevelFilter | str = LevelFilter.ERROR) -> None: + """ + Convenience function to direct all logging to a task local memory buffer. - [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) + [Rust `pactffi_log_to_buffer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_log_to_buffer) - The returned pointer must be deleted with - `pactffi_matching_rules_iter_delete` when done with it. + Raises: + RuntimeError: + If there was an error setting the logger. + """ + if isinstance(level_filter, str): + level_filter = LevelFilter[level_filter.upper()] + ret: int = lib.pactffi_log_to_buffer(level_filter.value) + if ret != 0: + msg = "There was an unknown error setting the logger." + raise RuntimeError(msg) - For sample, given the following rules: - ``` - "$.a" => Type, - "$.b" => Regex("\d+"), Number - ``` +def logger_init() -> None: + """ + Initialize the FFI logger with no sinks. - This iterator will return a sequence of 3 values: + [Rust `pactffi_logger_init`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_logger_init) - - `("$.a", Type)` - - `("$.b", Regex("\d+"))` - - `("$.b", Number)` + This initialized logger does nothing until `pactffi_logger_apply` has been called. - # Safety + # Usage - The iterator contains a copy of the data, so is safe to use when the - interaction or response contents has been deleted. + ```c + pactffi_logger_init(); + ``` - # Error Handling + # Safety - On failure, this function will return a NULL pointer. + This function is always safe to call. """ raise NotImplementedError -def message_contents_get_generators_iter( - contents: MessageContents, - category: GeneratorCategory, -) -> GeneratorCategoryIterator: +def logger_attach_sink(sink_specifier: str, level_filter: LevelFilter) -> int: """ - Get an iterator over the generators for a category of a message. + Attach an additional sink to the thread-local logger. [Rust - `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_contents_get_generators_iter) - - The returned pointer must be deleted with `pactffi_generators_iter_delete` - when done with it. - - # Safety + `pactffi_logger_attach_sink`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_logger_attach_sink) - The iterator contains a copy of the data, so is safe to use when the message - or message contents has been deleted. - - # Error Handling + This logger does nothing until `pactffi_logger_apply` has been called. - On failure, this function will return a NULL pointer. - """ - raise NotImplementedError + Types of sinks can be specified: + - stdout (`pactffi_logger_attach_sink("stdout", LevelFilter_Info)`) + - stderr (`pactffi_logger_attach_sink("stderr", LevelFilter_Debug)`) + - file w/ file path (`pactffi_logger_attach_sink("file /some/file/path", + LevelFilter_Trace)`) + - buffer (`pactffi_logger_attach_sink("buffer", LevelFilter_Debug)`) -def request_contents_get_generators_iter( - request: HttpRequest, - category: GeneratorCategory, -) -> GeneratorCategoryIterator: - """ - Get an iterator over the generators for a category of an HTTP request. + # Usage - [Rust - `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_request_contents_get_generators_iter) + ```c + int result = pactffi_logger_attach_sink("file /some/file/path", LogLevel_Filter); + ``` - The returned pointer must be deleted with `pactffi_generators_iter_delete` - when done with it. + # Error Handling - # Safety + The return error codes are as follows: - The iterator contains a copy of the data, so is safe to use when the - interaction or request contents has been deleted. + - `-1`: Can't set logger (applying the logger failed, perhaps because one is + applied already). + - `-2`: No logger has been initialized (call `pactffi_logger_init` before + any other log function). + - `-3`: The sink specifier was not UTF-8 encoded. + - `-4`: The sink type specified is not a known type (known types: "stdout", + "stderr", or "file /some/path"). + - `-5`: No file path was specified in a file-type sink specification. + - `-6`: Opening a sink to the specified file path failed (check + permissions). - # Error Handling + # Safety - On failure, this function will return a NULL pointer. + This function checks the validity of the passed-in sink specifier, and + errors out if the specifier isn't valid UTF-8. Passing in an invalid or NULL + pointer will result in undefined behaviour. """ raise NotImplementedError -def response_contents_get_generators_iter( - response: HttpResponse, - category: GeneratorCategory, -) -> GeneratorCategoryIterator: +def logger_apply() -> int: """ - Get an iterator over the generators for a category of an HTTP response. + Apply the previously configured sinks and levels to the program. [Rust - `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_response_contents_get_generators_iter) - - The returned pointer must be deleted with `pactffi_generators_iter_delete` - when done with it. - - # Safety + `pactffi_logger_apply`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_logger_apply) - The iterator contains a copy of the data, so is safe to use when the - interaction or response contents has been deleted. - - # Error Handling + If no sinks have been setup, will set the log level to info and the target + to standard out. - On failure, this function will return a NULL pointer. + This function will install a global tracing subscriber. Any attempts to + modify the logger after the call to `logger_apply` will fail. """ raise NotImplementedError -def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult: +def fetch_log_buffer(log_id: str) -> str: """ - Parse a matcher definition string into a MatchingRuleDefinition. - - The MatchingRuleDefition contains the example value, and matching rules and - any generator. + Fetch the in-memory logger buffer contents. [Rust - `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_parse_matcher_definition) + `pactffi_fetch_log_buffer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_fetch_log_buffer) - The following are examples of matching rule definitions: - - * `matching(type,'Name')` - type matcher with string value 'Name' - * `matching(number,100)` - number matcher - * `matching(datetime, 'yyyy-MM-dd','2000-01-01')` - datetime matcher with - format string - - See [Matching Rule definition - expressions](https://docs.rs/pact_models/latest/pact_models/matchingrules/expressions/index.html). + This will only have any contents if the `buffer` sink has been configured to + log to. The contents will be allocated on the heap and will need to be freed + with `pactffi_string_delete`. - The returned value needs to be freed up with the - `pactffi_matcher_definition_delete` function. + Fetches the logs associated with the provided identifier, or uses the + "global" one if the identifier is not specified (i.e. NULL). - # Errors If the expression is invalid, the MatchingRuleDefinition error will - be set. You can check for this value with the - `pactffi_matcher_definition_error` function. + Returns a NULL pointer if the buffer can't be fetched. This can occur is + there is not sufficient memory to make a copy of the contents or the buffer + contains non-UTF-8 characters. # Safety - This function is safe if the expression is a valid NULL terminated string - pointer. + This function will fail if the log_id pointer is invalid or does not point + to a NULL terminated string. """ raise NotImplementedError -def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str: +def parse_pact_json(json: str) -> Pact: """ - Returns any error message from parsing a matching definition expression. - - If there is no error, it will return a NULL pointer, otherwise returns the - error message as a NULL-terminated string. The returned string must be freed - using the `pactffi_string_delete` function once done with it. + Parses the provided JSON into a Pact model. [Rust - `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_error) - """ - raise NotImplementedError - - -def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str: - """ - Returns the value from parsing a matching definition expression. + `pactffi_parse_pact_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_parse_pact_json) - If there was an error, it will return a NULL pointer, otherwise returns the - value as a NULL-terminated string. The returned string must be freed using - the `pactffi_string_delete` function once done with it. + The returned Pact model must be freed with the `pactffi_pact_model_delete` + function when no longer needed. - [Rust - `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_value) + # Error Handling - Note that different expressions values can have types other than a string. - Use `pactffi_matcher_definition_value_type` to get the actual type of the - value. This function will always return the string representation of the - value. + This function will return a NULL pointer if passed a NULL pointer or if an + error occurs. """ raise NotImplementedError -def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None: +def pact_model_delete(pact: Pact) -> None: """ - Frees the memory used by the result of parsing the matching definition expression. + Frees the memory used by the Pact model. - [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_delete) + [Rust `pactffi_pact_model_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_model_delete) """ raise NotImplementedError -def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Generator: +def pact_model_interaction_iterator(pact: Pact) -> PactInteractionIterator: """ - Returns the generator from parsing a matching definition expression. - - If there was an error or there is no associated generator, it will return a - NULL pointer, otherwise returns the generator as a pointer. + Returns an iterator over all the interactions in the Pact. [Rust - `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_generator) - - The generator pointer will be a valid pointer as long as - `pactffi_matcher_definition_delete` has not been called on the definition. - Using the generator pointer after the definition has been deleted will - result in undefined behaviour. - """ - raise NotImplementedError + `pactffi_pact_model_interaction_iterator`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_model_interaction_iterator) + The iterator will have to be deleted using the + `pactffi_pact_interaction_iter_delete` function. The iterator will contain a + copy of the interactions, so it will not be affected but mutations to the + Pact model and will still function if the Pact model is deleted. -def matcher_definition_value_type( - definition: MatchingRuleDefinitionResult, -) -> ExpressionValueType: - """ - Returns the type of the value from parsing a matching definition expression. - - If there was an error parsing the expression, it will return Unknown. + # Safety This function is safe as long as the Pact pointer is a valid + pointer. - [Rust - `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_value_type) + # Errors On any error, this function will return a NULL pointer. """ raise NotImplementedError -def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None: +def pact_spec_version(pact: Pact) -> PactSpecification: """ - Free the iterator when you're done using it. + Returns the Pact specification enum that the Pact is for. - [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_iter_delete) + [Rust `pactffi_pact_spec_version`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_spec_version) """ raise NotImplementedError -def matcher_definition_iter( - definition: MatchingRuleDefinitionResult, -) -> MatchingRuleIterator: +def pact_interaction_delete(interaction: PactInteraction) -> None: """ - Returns an iterator over the matching rules from the parsed definition. - - The iterator needs to be deleted with the - `pactffi_matching_rule_iter_delete` function once done with it. - - [Rust - `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matcher_definition_iter) + Frees the memory used by the Pact interaction model. - If there was an error parsing the expression, this function will return a - NULL pointer. + [Rust `pactffi_pact_interaction_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_delete) """ raise NotImplementedError -def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: +def async_message_new() -> AsynchronousMessage: """ - Get the next matching rule or reference from the iterator. - - As the values returned are owned by the iterator, they do not need to be - deleted but will be cleaned up when the iterator is deleted. - - [Rust - `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_iter_next) + Get a mutable pointer to a newly-created default message on the heap. - Will return a NULL pointer when the iterator has advanced past the end of - the list. + [Rust `pactffi_async_message_new`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_new) # Safety @@ -2231,439 +2387,438 @@ def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: # Error Handling - This function will return a NULL pointer if passed a NULL pointer or if an - error occurs. + Returns NULL on error. """ raise NotImplementedError -def matching_rule_id(rule_result: MatchingRuleResult) -> int: +def async_message_delete(message: AsynchronousMessage) -> None: """ - Return the ID of the matching rule. - - [Rust - `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_id) - - The ID corresponds to the following rules: - - | Rule | ID | - | ---- | -- | - | Equality | 1 | - | Regex | 2 | - | Type | 3 | - | MinType | 4 | - | MaxType | 5 | - | MinMaxType | 6 | - | Timestamp | 7 | - | Time | 8 | - | Date | 9 | - | Include | 10 | - | Number | 11 | - | Integer | 12 | - | Decimal | 13 | - | Null | 14 | - | ContentType | 15 | - | ArrayContains | 16 | - | Values | 17 | - | Boolean | 18 | - | StatusCode | 19 | - | NotEmpty | 20 | - | Semver | 21 | - | EachKey | 22 | - | EachValue | 23 | - - # Safety + Destroy the `AsynchronousMessage` being pointed to. - This function is safe as long as the MatchingRuleResult pointer is a valid - pointer and the iterator has not been deleted. + [Rust `pactffi_async_message_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_delete) """ - raise NotImplementedError + lib.pactffi_async_message_delete(message._ptr) -def matching_rule_value(rule_result: MatchingRuleResult) -> str: +def async_message_get_contents(message: AsynchronousMessage) -> MessageContents | None: """ - Returns the associated value for the matching rule. - - If the matching rule does not have an associated value, will return a NULL - pointer. + Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. [Rust - `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_value) - - The associated values for the rules are: - - | Rule | ID | VALUE | - | ---- | -- | ----- | - | Equality | 1 | NULL | - | Regex | 2 | Regex value | - | Type | 3 | NULL | - | MinType | 4 | Minimum value | - | MaxType | 5 | Maximum value | - | MinMaxType | 6 | "min:max" | - | Timestamp | 7 | Format string | - | Time | 8 | Format string | - | Date | 9 | Format string | - | Include | 10 | String value | - | Number | 11 | NULL | - | Integer | 12 | NULL | - | Decimal | 13 | NULL | - | Null | 14 | NULL | - | ContentType | 15 | Content type | - | ArrayContains | 16 | NULL | - | Values | 17 | NULL | - | Boolean | 18 | NULL | - | StatusCode | 19 | NULL | - | NotEmpty | 20 | NULL | - | Semver | 21 | NULL | - | EachKey | 22 | NULL | - | EachValue | 23 | NULL | - - Will return a NULL pointer if the matching rule was a reference or does not - have an associated value. - - # Safety + `pactffi_async_message_get_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents) - This function is safe as long as the MatchingRuleResult pointer is a valid - pointer and the iterator it came from has not been deleted. + If the message contents are missing, this function will return `None`. """ - raise NotImplementedError + return MessageContents(lib.pactffi_async_message_get_contents(message._ptr)) -def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule: +def async_message_generate_contents( + message: AsynchronousMessage, +) -> MessageContents | None: """ - Returns the matching rule pointer for the matching rule. + Get the message contents of an `AsynchronousMessage` as a `MessageContents` pointer. - Will return a NULL pointer if the matching rule result was a reference. + This function differs from `async_message_get_contents` in + that it will process the message contents for any generators or matchers + that are present in the message in order to generate the actual message + contents as would be received by the consumer. [Rust - `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_pointer) + `pactffi_async_message_generate_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_generate_contents) - # Safety - - This function is safe as long as the MatchingRuleResult pointer is a valid - pointer and the iterator it came from has not been deleted. + If the message contents are missing, this function will return `None`. """ - raise NotImplementedError + return MessageContents( + lib.pactffi_async_message_generate_contents(message._ptr), + owned=False, + ) -def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: +def async_message_get_contents_str(message: AsynchronousMessage) -> str: """ - Return any matching rule reference to a attribute by name. + Get the message contents of an `AsynchronousMessage` in string form. - This is when the matcher should be configured to match the type of a - structure. I.e., + [Rust `pactffi_async_message_get_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents_str) - [Rust - `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_reference_name) + # Safety - ```json - { - "pact:match": "eachValue(matching($'person'))", - "person": { - "name": "Fred", - "age": 100 - } - } - ``` + The returned string must be deleted with `pactffi_string_delete`. - Will return a NULL pointer if the matching rule was not a reference. + The returned string can outlive the message. - # Safety + # Error Handling - This function is safe as long as the MatchingRuleResult pointer is a valid - pointer and the iterator has not been deleted. + If the message is NULL, returns NULL. If the body of the message + is missing, then this function also returns NULL. This means there's + no mechanism to differentiate with this function call alone between + a NULL message and a missing message body. """ raise NotImplementedError -def validate_datetime(value: str, format: str) -> None: +def async_message_set_contents_str( + message: AsynchronousMessage, + contents: str, + content_type: str, +) -> None: """ - Validates the date/time value against the date/time format string. - - Raises an error if the value is not a valid date/time for the format string. - - If the value is valid, this function will return a zero status code - (EXIT_SUCCESS). If the value is not valid, will return a value of 1 - (EXIT_FAILURE) and set the error message which can be retrieved with - `pactffi_get_error_message`. + Sets the contents of the message as a string. [Rust - `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_validate_datetime) + `pactffi_async_message_set_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_set_contents_str) - # Errors If the function receives a panic, it will return 2 and the message - associated with the panic can be retrieved with `pactffi_get_error_message`. + - `message` - the message to set the contents for + - `contents` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. # Safety - This function is safe as long as the value and format parameters point to - valid NULL-terminated strings. + The message contents and content type must either be NULL pointers, or point + to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. """ - ret = lib.pactffi_validate_datetime(value.encode(), format.encode()) - if ret == 0: - return - if ret == 1: - msg = f"Invalid datetime value {value!r}' for format {format!r}" - raise ValueError(msg) - if ret == 2: # noqa: PLR2004 - msg = f"Panic while validating datetime value: {get_error_message()}" - else: - msg = f"Unknown error while validating datetime value: {ret}" - raise RuntimeError(msg) + raise NotImplementedError -def generator_to_json(generator: Generator) -> str: +def async_message_get_contents_length(message: AsynchronousMessage) -> int: """ - Get the JSON form of the generator. + Get the length of the contents of a `AsynchronousMessage`. [Rust - `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generator_to_json) - - The returned string must be deleted with `pactffi_string_delete`. + `pactffi_async_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents_length) # Safety - This function will fail if it is passed a NULL pointer, or the owner of the - generator has been deleted. + This function is safe. + + # Error Handling + + If the message is NULL, returns 0. If the body of the request is missing, + then this function also returns 0. """ raise NotImplementedError -def generator_generate_string(generator: Generator, context_json: str) -> str: +def async_message_get_contents_bin(message: AsynchronousMessage) -> str: """ - Generate a string value using the provided generator. - - An optional JSON payload containing any generator context ca be given. The - context value is used for generators like `MockServerURL` (which should - contain details about the running mock server) and `ProviderStateGenerator` - (which should be the values returned from the Provider State callback - function). + Get the contents of an `AsynchronousMessage` as bytes. [Rust - `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generator_generate_string) + `pactffi_async_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_contents_bin) - If anything goes wrong, it will return a NULL pointer. + # Safety + + The number of bytes in the buffer will be returned by + `pactffi_async_message_get_contents_length`. It is safe to use the pointer + while the message is not deleted or changed. Using the pointer after the + message is mutated or deleted may lead to undefined behaviour. + + # Error Handling + + If the message is NULL, returns NULL. If the body of the message is missing, + then this function also returns NULL. """ raise NotImplementedError -def generator_generate_integer(generator: Generator, context_json: str) -> int: +def async_message_set_contents_bin( + message: AsynchronousMessage, + contents: str, + len: int, + content_type: str, +) -> None: """ - Generate an integer value using the provided generator. - - An optional JSON payload containing any generator context can be given. The - context value is used for generators like `ProviderStateGenerator` (which - should be the values returned from the Provider State callback function). + Sets the contents of the message as an array of bytes. [Rust - `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generator_generate_integer) + `pactffi_async_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_set_contents_bin) - If anything goes wrong or the generator is not a type that can generate an - integer value, it will return a zero value. + * `message` - the message to set the contents for + * `contents` - pointer to contents to copy from + * `len` - number of bytes to copy from the contents pointer + * `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. + + # Safety + + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. + + # Error Handling + + If the contents is a NULL pointer, it will set the message contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. """ raise NotImplementedError -def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: - """ - Free the iterator when you're done using it. +def async_message_get_description(message: AsynchronousMessage) -> str: + r""" + Get a copy of the description. [Rust - `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generators_iter_delete) + `pactffi_async_message_get_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_description) + + Raises: + RuntimeError: + If the description cannot be retrieved. """ - raise NotImplementedError + ptr = lib.pactffi_async_message_get_description(message._ptr) + if ptr == ffi.NULL: + msg = "Unable to get the description from the message." + raise RuntimeError(msg) + return OwnedString(ptr) -def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePair: +def async_message_set_description( + message: AsynchronousMessage, + description: str, +) -> int: """ - Get the next path and generator out of the iterator, if possible. - - [Rust - `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generators_iter_next) + Write the `description` field on the `AsynchronousMessage`. - The returned pointer must be deleted with - `pactffi_generator_iter_pair_delete`. + [Rust `pactffi_async_message_set_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_set_description) # Safety - The underlying data is owned by the `GeneratorKeyValuePair`, so is always - safe to use. + `description` must contain valid UTF-8. Invalid UTF-8 + will be replaced with U+FFFD REPLACEMENT CHARACTER. + + This function will only reallocate if the new string + does not fit in the existing buffer. # Error Handling - If no further data is present, returns NULL. + Errors will be reported with a non-zero return value. """ raise NotImplementedError -def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: - """ - Free a pair of key and value returned from `pactffi_generators_iter_next`. +def async_message_get_provider_state( + message: AsynchronousMessage, + index: int, +) -> ProviderState: + r""" + Get a copy of the provider state at the given index from this message. [Rust - `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generators_iter_pair_delete) + `pactffi_async_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_provider_state) + + Raises: + RuntimeError: + If the provider state cannot be retrieved. """ - raise NotImplementedError + ptr = lib.pactffi_async_message_get_provider_state(message._ptr, index) + if ptr == ffi.NULL: + msg = "Unable to get the provider state from the message." + raise RuntimeError(msg) + return ProviderState(ptr) -def sync_http_new() -> SynchronousHttp: +def async_message_get_provider_state_iter( + message: AsynchronousMessage, +) -> ProviderStateIterator: """ - Get a mutable pointer to a newly-created default interaction on the heap. + Get an iterator over provider states. - [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_new) + [Rust `pactffi_async_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_async_message_get_provider_state_iter) # Safety - This function is safe. + The underlying data must not change during iteration. + """ + return ProviderStateIterator( + lib.pactffi_async_message_get_provider_state_iter(message._ptr) + ) - # Error Handling - Returns NULL on error. - """ - raise NotImplementedError +def consumer_get_name(consumer: Consumer) -> str: + r""" + Get a copy of this consumer's name. + [Rust `pactffi_consumer_get_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_consumer_get_name) -def sync_http_delete(interaction: SynchronousHttp) -> None: - """ - Destroy the `SynchronousHttp` interaction being pointed to. + The copy must be deleted with `pactffi_string_delete`. - [Rust - `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_delete) + # Usage + + ```c + // Assuming `file_name` and `json_str` are already defined. + + MessagePact *message_pact = pactffi_message_pact_new_from_json(file_name, json_str); + if (message_pact == NULLPTR) { + // handle error. + } + + Consumer *consumer = pactffi_message_pact_get_consumer(message_pact); + if (consumer == NULLPTR) { + // handle error. + } + + char *name = pactffi_consumer_get_name(consumer); + if (name == NULL) { + // handle error. + } + + printf("%s\n", name); + + pactffi_string_delete(name); + ``` + + # Errors + + This function will fail if it is passed a NULL pointer, + or the Rust string contains an embedded NULL byte. + In the case of error, a NULL pointer will be returned. """ raise NotImplementedError -def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: +def pact_get_consumer(pact: Pact) -> Consumer: """ - Get the request of a `SynchronousHttp` interaction. + Get the consumer from a Pact. + + This returns a copy of the consumer model, and needs to be cleaned up with + `pactffi_pact_consumer_delete` when no longer required. [Rust - `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request) + `pactffi_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_get_consumer) - # Safety + # Errors - The data pointed to by the pointer this function returns will be deleted - when the interaction is deleted. Trying to use if after the interaction is - deleted will result in undefined behaviour. + This function will fail if it is passed a NULL pointer. In the case of + error, a NULL pointer will be returned. + """ + raise NotImplementedError - # Error Handling - If the interaction is NULL, returns NULL. +def pact_consumer_delete(consumer: Consumer) -> None: + """ + Frees the memory used by the Pact consumer. + + [Rust `pactffi_pact_consumer_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_consumer_delete) """ raise NotImplementedError -def sync_http_get_request_contents(interaction: SynchronousHttp) -> str: +def message_contents_delete(contents: MessageContents) -> None: """ - Get the request contents of a `SynchronousHttp` interaction in string form. + Delete the message contents instance. - [Rust - `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request_contents) + This should only be called on a message contents that require deletion. + The function creating the message contents should document whether it + requires deletion. - # Safety + Deleting a message content which is associated with an interaction + will result in undefined behaviour. - The returned string must be deleted with `pactffi_string_delete`. + [Rust `pactffi_message_contents_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_delete) + """ + lib.pactffi_message_contents_delete(contents._ptr) - The returned string can outlive the interaction. - # Error Handling +def message_contents_get_contents_str(contents: MessageContents) -> str | None: + """ + Get the message contents in string form. - If the interaction is NULL, returns NULL. If the body of the request is - missing, then this function also returns NULL. This means there's no - mechanism to differentiate with this function call alone between a NULL body - and a missing body. + [Rust `pactffi_message_contents_get_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_contents_str) + + If the message has no contents or contain invalid UTF-8 characters, this + function will return `None`. """ - raise NotImplementedError + ptr = lib.pactffi_message_contents_get_contents_str(contents._ptr) + if ptr == ffi.NULL: + return None + return OwnedString(ptr) -def sync_http_set_request_contents( - interaction: SynchronousHttp, - contents: str, +def message_contents_set_contents_str( + contents: MessageContents, + contents_str: str, content_type: str, ) -> None: """ - Sets the request contents of the interaction. + Sets the contents of the message as a string. [Rust - `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_request_contents) + `pactffi_message_contents_set_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_set_contents_str) - - `interaction` - the interaction to set the request contents for - - `contents` - pointer to contents to copy from. Must be a valid + * `contents` - the message contents to set the contents for + * `contents_str` - pointer to contents to copy from. Must be a valid NULL-terminated UTF-8 string pointer. - - `content_type` - pointer to the NULL-terminated UTF-8 string containing + * `content_type` - pointer to the NULL-terminated UTF-8 string containing the content type of the data. # Safety - The request contents and content type must either be NULL pointers, or point + The message contents and content type must either be NULL pointers, or point to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is undefined. # Error Handling - If the contents is a NULL pointer, it will set the request contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. + If the contents string is a NULL pointer, it will set the message contents + as null. If the content type is a null pointer, or can't be parsed, it will + set the content type as unknown. """ raise NotImplementedError -def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: +def message_contents_get_contents_length(contents: MessageContents) -> int: """ - Get the length of the request contents of a `SynchronousHttp` interaction. - - [Rust - `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) - - # Safety - - This function is safe. + Get the length of the message contents. - # Error Handling + [Rust `pactffi_message_contents_get_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_contents_length) - If the interaction is NULL, returns 0. If the body of the request is - missing, then this function also returns 0. + If the message has not contents, this function will return 0. """ - raise NotImplementedError + return lib.pactffi_message_contents_get_contents_length(contents._ptr) -def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes: +def message_contents_get_contents_bin(contents: MessageContents) -> bytes | None: """ - Get the request contents of a `SynchronousHttp` interaction as bytes. + Get the contents of a message as a pointer to an array of bytes. [Rust - `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) - - # Safety - - The number of bytes in the buffer will be returned by - `pactffi_sync_http_get_request_contents_length`. It is safe to use the - pointer while the interaction is not deleted or changed. Using the pointer - after the interaction is mutated or deleted may lead to undefined behaviour. - - # Error Handling + `pactffi_message_contents_get_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_contents_bin) - If the interaction is NULL, returns NULL. If the body of the request is - missing, then this function also returns NULL. + If the message has no contents, this function will return `None`. """ - raise NotImplementedError + ptr = lib.pactffi_message_contents_get_contents_bin(contents._ptr) + if ptr == ffi.NULL: + return None + return ffi.buffer( + ptr, + lib.pactffi_message_contents_get_contents_length(contents._ptr), + )[:] -def sync_http_set_request_contents_bin( - interaction: SynchronousHttp, - contents: str, +def message_contents_set_contents_bin( + contents: MessageContents, + contents_bin: str, len: int, content_type: str, ) -> None: """ - Sets the request contents of the interaction as an array of bytes. + Sets the contents of the message as an array of bytes. [Rust - `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) + `pactffi_message_contents_set_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_set_contents_bin) - - `interaction` - the interaction to set the request contents for - - `contents` - pointer to contents to copy from - - `len` - number of bytes to copy from the contents pointer - - `content_type` - pointer to the NULL-terminated UTF-8 string containing + * `message` - the message contents to set the contents for + * `contents_bin` - pointer to contents to copy from + * `len` - number of bytes to copy from the contents pointer + * `content_type` - pointer to the NULL-terminated UTF-8 string containing the content type of the data. # Safety @@ -2673,671 +2828,937 @@ def sync_http_set_request_contents_bin( # Error Handling - If the contents is a NULL pointer, it will set the request contents as null. + If the contents is a NULL pointer, it will set the message contents as null. If the content type is a null pointer, or can't be parsed, it will set the content type as unknown. """ raise NotImplementedError -def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: - """ - Get the response of a `SynchronousHttp` interaction. +def message_contents_get_metadata_iter( + contents: MessageContents, +) -> MessageMetadataIterator: + r""" + Get an iterator over the metadata of a message. [Rust - `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response) + `pactffi_message_contents_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_metadata_iter) # Safety - The data pointed to by the pointer this function returns will be deleted - when the interaction is deleted. Trying to use if after the interaction is - deleted will result in undefined behaviour. + This iterator carries a pointer to the message contents, and must not + outlive the message. - # Error Handling + The message metadata also must not be modified during iteration. If it is, + the old iterator must be deleted and a new iterator created. - If the interaction is NULL, returns NULL. + Raises: + RuntimeError: + If the metadata iterator cannot be retrieved. """ - raise NotImplementedError + ptr = lib.pactffi_message_contents_get_metadata_iter(contents._ptr) + if ptr == ffi.NULL: + msg = "Unable to get the metadata iterator from the message contents." + raise RuntimeError(msg) + return MessageMetadataIterator(ptr) -def sync_http_get_response_contents(interaction: SynchronousHttp) -> str: - """ - Get the response contents of a `SynchronousHttp` interaction in string form. +def message_contents_get_matching_rule_iter( + contents: MessageContents, + category: MatchingRuleCategory, +) -> MatchingRuleCategoryIterator: + r""" + Get an iterator over the matching rules for a category of a message. [Rust - `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response_contents) + `pactffi_message_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_matching_rule_iter) - # Safety + The returned pointer must be deleted with + `pactffi_matching_rules_iter_delete` when done with it. - The returned string must be deleted with `pactffi_string_delete`. + Note that there could be multiple matching rules for the same key, so this + iterator will sequentially return each rule with the same key. + + For sample, given the following rules: + + ``` + "$.a" => Type, + "$.b" => Regex("\\d+"), Number + ``` + + This iterator will return a sequence of 3 values: + + - `("$.a", Type)` + - `("$.b", Regex("\d+"))` + - `("$.b", Number)` + + # Safety - The returned string can outlive the interaction. + The iterator contains a copy of the data, so is safe to use when the message + or message contents has been deleted. # Error Handling - If the interaction is NULL, returns NULL. + On failure, this function will return a NULL pointer. + """ + return MatchingRuleCategoryIterator( + lib.pactffi_message_contents_get_matching_rule_iter(contents._ptr, category) + ) + + +def request_contents_get_matching_rule_iter( + request: HttpRequest, + category: MatchingRuleCategory, +) -> MatchingRuleCategoryIterator: + r""" + Get an iterator over the matching rules for a category of an HTTP request. + + [Rust `pactffi_request_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_request_contents_get_matching_rule_iter) + + The returned pointer must be deleted with + `pactffi_matching_rules_iter_delete` when done with it. + + For sample, given the following rules: + + ``` + "$.a" => Type, + "$.b" => Regex("\d+"), Number + ``` + + This iterator will return a sequence of 3 values: + + - `("$.a", Type)` + - `("$.b", Regex("\d+"))` + - `("$.b", Number)` + + # Safety + + The iterator contains a copy of the data, so is safe to use when the + interaction or request contents has been deleted. - If the body of the response is missing, then this function also returns - NULL. This means there's no mechanism to differentiate with this function - call alone between a NULL body and a missing body. + # Error Handling + + On failure, this function will return a NULL pointer. """ raise NotImplementedError -def sync_http_set_response_contents( - interaction: SynchronousHttp, - contents: str, - content_type: str, -) -> None: - """ - Sets the response contents of the interaction. +def response_contents_get_matching_rule_iter( + response: HttpResponse, + category: MatchingRuleCategory, +) -> MatchingRuleCategoryIterator: + r""" + Get an iterator over the matching rules for a category of an HTTP response. - [Rust - `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_response_contents) + [Rust `pactffi_response_contents_get_matching_rule_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_contents_get_matching_rule_iter) - - `interaction` - the interaction to set the response contents for - - `contents` - pointer to contents to copy from. Must be a valid - NULL-terminated UTF-8 string pointer. - - `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. + The returned pointer must be deleted with + `pactffi_matching_rules_iter_delete` when done with it. + + For sample, given the following rules: + + ``` + "$.a" => Type, + "$.b" => Regex("\d+"), Number + ``` + + This iterator will return a sequence of 3 values: + + - `("$.a", Type)` + - `("$.b", Regex("\d+"))` + - `("$.b", Number)` # Safety - The response contents and content type must either be NULL pointers, or - point to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is - undefined. + The iterator contains a copy of the data, so is safe to use when the + interaction or response contents has been deleted. # Error Handling - If the contents is a NULL pointer, it will set the response contents as - null. If the content type is a null pointer, or can't be parsed, it will set - the content type as unknown. + On failure, this function will return a NULL pointer. """ raise NotImplementedError -def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: +def message_contents_get_generators_iter( + contents: MessageContents, + category: GeneratorCategory, +) -> GeneratorCategoryIterator: """ - Get the length of the response contents of a `SynchronousHttp` interaction. + Get an iterator over the generators for a category of a message. [Rust - `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) + `pactffi_message_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_contents_get_generators_iter) # Safety - This function is safe. + The iterator contains a copy of the data, so is safe to use when the message + or message contents has been deleted. + + Raises: + RuntimeError: + If the generators iterator cannot be retrieved. + """ + ptr = lib.pactffi_message_contents_get_generators_iter(contents._ptr, category) + if ptr == ffi.NULL: + msg = "Unable to get the generators iterator from the message contents." + raise RuntimeError(msg) + return GeneratorCategoryIterator(ptr) + + +def request_contents_get_generators_iter( + request: HttpRequest, + category: GeneratorCategory, +) -> GeneratorCategoryIterator: + """ + Get an iterator over the generators for a category of an HTTP request. + + [Rust + `pactffi_request_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_request_contents_get_generators_iter) + + The returned pointer must be deleted with `pactffi_generators_iter_delete` + when done with it. + + # Safety + + The iterator contains a copy of the data, so is safe to use when the + interaction or request contents has been deleted. # Error Handling - If the interaction is NULL or the index is not valid, returns 0. If the body - of the response is missing, then this function also returns 0. + On failure, this function will return a NULL pointer. """ raise NotImplementedError -def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes: +def response_contents_get_generators_iter( + response: HttpResponse, + category: GeneratorCategory, +) -> GeneratorCategoryIterator: """ - Get the response contents of a `SynchronousHttp` interaction as bytes. + Get an iterator over the generators for a category of an HTTP response. [Rust - `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) + `pactffi_response_contents_get_generators_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_contents_get_generators_iter) + + The returned pointer must be deleted with `pactffi_generators_iter_delete` + when done with it. # Safety - The number of bytes in the buffer will be returned by - `pactffi_sync_http_get_response_contents_length`. It is safe to use the - pointer while the interaction is not deleted or changed. Using the pointer - after the interaction is mutated or deleted may lead to undefined behaviour. + The iterator contains a copy of the data, so is safe to use when the + interaction or response contents has been deleted. # Error Handling - If the interaction is NULL, returns NULL. If the body of the response is - missing, then this function also returns NULL. + On failure, this function will return a NULL pointer. """ raise NotImplementedError -def sync_http_set_response_contents_bin( - interaction: SynchronousHttp, - contents: str, - len: int, - content_type: str, -) -> None: +def parse_matcher_definition(expression: str) -> MatchingRuleDefinitionResult: """ - Sets the response contents of the `SynchronousHttp` interaction as bytes. + Parse a matcher definition string into a MatchingRuleDefinition. + + The MatchingRuleDefition contains the example value, and matching rules and + any generator. [Rust - `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) + `pactffi_parse_matcher_definition`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_parse_matcher_definition) - - `interaction` - the interaction to set the response contents for - - `contents` - pointer to contents to copy from - - `len` - number of bytes to copy - - `content_type` - pointer to the NULL-terminated UTF-8 string containing - the content type of the data. + The following are examples of matching rule definitions: - # Safety + * `matching(type,'Name')` - type matcher with string value 'Name' + * `matching(number,100)` - number matcher + * `matching(datetime, 'yyyy-MM-dd','2000-01-01')` - datetime matcher with + format string - The contents pointer must be valid for reads of `len` bytes, and it must be - properly aligned and consecutive. Otherwise behaviour is undefined. + See [Matching Rule definition + expressions](https://docs.rs/pact_models/latest/pact_models/matchingrules/expressions/index.html). - # Error Handling + The returned value needs to be freed up with the + `pactffi_matcher_definition_delete` function. - If the contents is a NULL pointer, it will set the response contents as - null. If the content type is a null pointer, or can't be parsed, it will set - the content type as unknown. + # Errors If the expression is invalid, the MatchingRuleDefinition error will + be set. You can check for this value with the + `pactffi_matcher_definition_error` function. + + # Safety + + This function is safe if the expression is a valid NULL terminated string + pointer. """ raise NotImplementedError -def sync_http_get_description(interaction: SynchronousHttp) -> str: - r""" - Get a copy of the description. +def matcher_definition_error(definition: MatchingRuleDefinitionResult) -> str: + """ + Returns any error message from parsing a matching definition expression. - [Rust - `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_description) + If there is no error, it will return a NULL pointer, otherwise returns the + error message as a NULL-terminated string. The returned string must be freed + using the `pactffi_string_delete` function once done with it. - # Safety + [Rust + `pactffi_matcher_definition_error`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_error) + """ + raise NotImplementedError - The returned string must be deleted with `pactffi_string_delete`. - Since it is a copy, the returned string may safely outlive the - `SynchronousHttp` interaction. +def matcher_definition_value(definition: MatchingRuleDefinitionResult) -> str: + """ + Returns the value from parsing a matching definition expression. - # Errors + If there was an error, it will return a NULL pointer, otherwise returns the + value as a NULL-terminated string. The returned string must be freed using + the `pactffi_string_delete` function once done with it. - On failure, this function will return a NULL pointer. + [Rust + `pactffi_matcher_definition_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_value) - This function may fail if the Rust string contains embedded null ('\0') - bytes. + Note that different expressions values can have types other than a string. + Use `pactffi_matcher_definition_value_type` to get the actual type of the + value. This function will always return the string representation of the + value. """ raise NotImplementedError -def sync_http_set_description(interaction: SynchronousHttp, description: str) -> int: +def matcher_definition_delete(definition: MatchingRuleDefinitionResult) -> None: """ - Write the `description` field on the `SynchronousHttp`. + Frees the memory used by the result of parsing the matching definition expression. - [Rust - `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_set_description) + [Rust `pactffi_matcher_definition_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_delete) + """ + raise NotImplementedError - # Safety - `description` must contain valid UTF-8. Invalid UTF-8 will be replaced with - U+FFFD REPLACEMENT CHARACTER. +def matcher_definition_generator(definition: MatchingRuleDefinitionResult) -> Generator: + """ + Returns the generator from parsing a matching definition expression. - This function will only reallocate if the new string does not fit in the - existing buffer. + If there was an error or there is no associated generator, it will return a + NULL pointer, otherwise returns the generator as a pointer. - # Error Handling + [Rust + `pactffi_matcher_definition_generator`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_generator) - Errors will be reported with a non-zero return value. + The generator pointer will be a valid pointer as long as + `pactffi_matcher_definition_delete` has not been called on the definition. + Using the generator pointer after the definition has been deleted will + result in undefined behaviour. """ raise NotImplementedError -def sync_http_get_provider_state( - interaction: SynchronousHttp, - index: int, -) -> ProviderState: - r""" - Get a copy of the provider state at the given index from this interaction. +def matcher_definition_value_type( + definition: MatchingRuleDefinitionResult, +) -> ExpressionValueType: + """ + Returns the type of the value from parsing a matching definition expression. + + If there was an error parsing the expression, it will return Unknown. [Rust - `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_provider_state) + `pactffi_matcher_definition_value_type`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_value_type) + """ + raise NotImplementedError - # Safety - The returned structure must be deleted with `provider_state_delete`. +def matching_rule_iter_delete(iter: MatchingRuleIterator) -> None: + """ + Free the iterator when you're done using it. - Since it is a copy, the returned structure may safely outlive the - `SynchronousHttp`. + [Rust `pactffi_matching_rule_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_iter_delete) + """ + raise NotImplementedError - # Error Handling - On failure, this function will return a variant other than Success. +def matcher_definition_iter( + definition: MatchingRuleDefinitionResult, +) -> MatchingRuleIterator: + """ + Returns an iterator over the matching rules from the parsed definition. - This function may fail if the index requested is out of bounds, or if any of - the Rust strings contain embedded null ('\0') bytes. + The iterator needs to be deleted with the + `pactffi_matching_rule_iter_delete` function once done with it. + + [Rust + `pactffi_matcher_definition_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matcher_definition_iter) + + If there was an error parsing the expression, this function will return a + NULL pointer. """ raise NotImplementedError -def sync_http_get_provider_state_iter( - interaction: SynchronousHttp, -) -> ProviderStateIterator: +def matching_rule_iter_next(iter: MatchingRuleIterator) -> MatchingRuleResult: """ - Get an iterator over provider states. + Get the next matching rule or reference from the iterator. + + As the values returned are owned by the iterator, they do not need to be + deleted but will be cleaned up when the iterator is deleted. [Rust - `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) + `pactffi_matching_rule_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_iter_next) + + Will return a NULL pointer when the iterator has advanced past the end of + the list. # Safety - The underlying data must not change during iteration. + This function is safe. # Error Handling - Returns NULL if an error occurs. + This function will return a NULL pointer if passed a NULL pointer or if an + error occurs. """ raise NotImplementedError -def pact_interaction_as_synchronous_http( - interaction: PactInteraction, -) -> SynchronousHttp: - r""" - Casts this interaction to a `SynchronousHttp` interaction. - - Returns a NULL pointer if the interaction can not be casted to a - `SynchronousHttp` interaction (for instance, it is a message interaction). - The returned pointer must be freed with `pactffi_sync_http_delete` when no - longer required. +def matching_rule_id(rule_result: MatchingRuleResult) -> int: + """ + Return the ID of the matching rule. [Rust - `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) + `pactffi_matching_rule_id`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_id) - # Safety This function is safe as long as the interaction pointer is a valid - pointer. + The ID corresponds to the following rules: - # Errors On any error, this function will return a NULL pointer. + | Rule | ID | + | ---- | -- | + | Equality | 1 | + | Regex | 2 | + | Type | 3 | + | MinType | 4 | + | MaxType | 5 | + | MinMaxType | 6 | + | Timestamp | 7 | + | Time | 8 | + | Date | 9 | + | Include | 10 | + | Number | 11 | + | Integer | 12 | + | Decimal | 13 | + | Null | 14 | + | ContentType | 15 | + | ArrayContains | 16 | + | Values | 17 | + | Boolean | 18 | + | StatusCode | 19 | + | NotEmpty | 20 | + | Semver | 21 | + | EachKey | 22 | + | EachValue | 23 | + + # Safety + + This function is safe as long as the MatchingRuleResult pointer is a valid + pointer and the iterator has not been deleted. """ raise NotImplementedError -def pact_interaction_as_message(interaction: PactInteraction) -> Message: +def matching_rule_value(rule_result: MatchingRuleResult) -> str: """ - Casts this interaction to a `Message` interaction. + Returns the associated value for the matching rule. - Returns a NULL pointer if the interaction can not be casted to a `Message` - interaction (for instance, it is a http interaction). The returned pointer - must be freed with `pactffi_message_delete` when no longer required. + If the matching rule does not have an associated value, will return a NULL + pointer. [Rust - `pactffi_pact_interaction_as_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_as_message) + `pactffi_matching_rule_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_value) - Note that if the interaction is a V4 `AsynchronousMessage`, it will be - converted to a V3 `Message` before being returned. + The associated values for the rules are: - # Safety This function is safe as long as the interaction pointer is a valid - pointer. + | Rule | ID | VALUE | + | ---- | -- | ----- | + | Equality | 1 | NULL | + | Regex | 2 | Regex value | + | Type | 3 | NULL | + | MinType | 4 | Minimum value | + | MaxType | 5 | Maximum value | + | MinMaxType | 6 | "min:max" | + | Timestamp | 7 | Format string | + | Time | 8 | Format string | + | Date | 9 | Format string | + | Include | 10 | String value | + | Number | 11 | NULL | + | Integer | 12 | NULL | + | Decimal | 13 | NULL | + | Null | 14 | NULL | + | ContentType | 15 | Content type | + | ArrayContains | 16 | NULL | + | Values | 17 | NULL | + | Boolean | 18 | NULL | + | StatusCode | 19 | NULL | + | NotEmpty | 20 | NULL | + | Semver | 21 | NULL | + | EachKey | 22 | NULL | + | EachValue | 23 | NULL | - # Errors On any error, this function will return a NULL pointer. + Will return a NULL pointer if the matching rule was a reference or does not + have an associated value. + + # Safety + + This function is safe as long as the MatchingRuleResult pointer is a valid + pointer and the iterator it came from has not been deleted. """ raise NotImplementedError -def pact_interaction_as_asynchronous_message( - interaction: PactInteraction, -) -> AsynchronousMessage: +def matching_rule_pointer(rule_result: MatchingRuleResult) -> MatchingRule: """ - Casts this interaction to a `AsynchronousMessage` interaction. - - Returns a NULL pointer if the interaction can not be casted to a - `AsynchronousMessage` interaction (for instance, it is a http interaction). - The returned pointer must be freed with `pactffi_async_message_delete` when - no longer required. + Returns the matching rule pointer for the matching rule. - [Rust - `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) + Will return a NULL pointer if the matching rule result was a reference. - Note that if the interaction is a V3 `Message`, it will be converted to a V4 - `AsynchronousMessage` before being returned. + [Rust + `pactffi_matching_rule_pointer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_pointer) - # Safety This function is safe as long as the interaction pointer is a valid - pointer. + # Safety - # Errors On any error, this function will return a NULL pointer. + This function is safe as long as the MatchingRuleResult pointer is a valid + pointer and the iterator it came from has not been deleted. """ raise NotImplementedError -def pact_interaction_as_synchronous_message( - interaction: PactInteraction, -) -> SynchronousMessage: +def matching_rule_reference_name(rule_result: MatchingRuleResult) -> str: """ - Casts this interaction to a `SynchronousMessage` interaction. + Return any matching rule reference to a attribute by name. - Returns a NULL pointer if the interaction can not be casted to a - `SynchronousMessage` interaction (for instance, it is a http interaction). - The returned pointer must be freed with `pactffi_sync_message_delete` when - no longer required. + This is when the matcher should be configured to match the type of a + structure. I.e., [Rust - `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) + `pactffi_matching_rule_reference_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_reference_name) - # Safety This function is safe as long as the interaction pointer is a valid - pointer. + ```json + { + "pact:match": "eachValue(matching($'person'))", + "person": { + "name": "Fred", + "age": 100 + } + } + ``` - # Errors On any error, this function will return a NULL pointer. + Will return a NULL pointer if the matching rule was not a reference. + + # Safety + + This function is safe as long as the MatchingRuleResult pointer is a valid + pointer and the iterator has not been deleted. """ raise NotImplementedError -def pact_message_iter_delete(iter: PactMessageIterator) -> None: +def validate_datetime(value: str, format: str) -> None: """ - Free the iterator when you're done using it. + Validates the date/time value against the date/time format string. [Rust - `pactffi_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_message_iter_delete) + `pactffi_validate_datetime`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_validate_datetime) + + Raises: + ValueError: + If the value is not a valid date/time for the format string. + + RuntimeError: + For any other error. """ - lib.pactffi_pact_message_iter_delete(iter._ptr) + ret = lib.pactffi_validate_datetime(value.encode(), format.encode()) + if ret == 0: + return + if ret == 1: + msg = f"Invalid datetime value {value!r}' for format {format!r}" + raise ValueError(msg) + if ret == 2: # noqa: PLR2004 + msg = f"Panic while validating datetime value: {get_error_message()}" + else: + msg = f"Unknown error while validating datetime value: {ret}" + raise RuntimeError(msg) -def pact_message_iter_next(iter: PactMessageIterator) -> Message: +def generator_to_json(generator: Generator) -> str: """ - Get the next message from the message pact. + Get the JSON form of the generator. [Rust - `pactffi_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_message_iter_next) - """ - ptr = lib.pactffi_pact_message_iter_next(iter._ptr) - if ptr == ffi.NULL: - raise StopIteration - raise NotImplementedError - return Message(ptr) + `pactffi_generator_to_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generator_to_json) + The returned string must be deleted with `pactffi_string_delete`. -def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMessage: - """ - Get the next synchronous request/response message from the V4 pact. + # Safety - [Rust - `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_message_iter_next) + This function will fail if it is passed a NULL pointer, or the owner of the + generator has been deleted. """ - ptr = lib.pactffi_pact_sync_message_iter_next(iter._ptr) - if ptr == ffi.NULL: - raise StopIteration - raise NotImplementedError - return SynchronousMessage(ptr) + return OwnedString(lib.pactffi_generator_to_json(generator._ptr)) -def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: +def generator_generate_string(generator: Generator, context_json: str) -> str: """ - Free the iterator when you're done using it. + Generate a string value using the provided generator. + + An optional JSON payload containing any generator context ca be given. The + context value is used for generators like `MockServerURL` (which should + contain details about the running mock server) and `ProviderStateGenerator` + (which should be the values returned from the Provider State callback + function). [Rust - `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) + `pactffi_generator_generate_string`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generator_generate_string) + + If anything goes wrong, it will return a NULL pointer. """ - lib.pactffi_pact_sync_message_iter_delete(iter._ptr) + ptr = lib.pactffi_generator_generate_string( + generator._ptr, + context_json.encode("utf-8"), + ) + s = ffi.string(ptr) + if isinstance(s, bytes): + s = s.decode("utf-8") + return s -def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: +def generator_generate_integer(generator: Generator, context_json: str) -> int: """ - Get the next synchronous HTTP request/response interaction from the V4 pact. + Generate an integer value using the provided generator. + + An optional JSON payload containing any generator context can be given. The + context value is used for generators like `ProviderStateGenerator` (which + should be the values returned from the Provider State callback function). [Rust - `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_http_iter_next) + `pactffi_generator_generate_integer`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generator_generate_integer) + + If anything goes wrong or the generator is not a type that can generate an + integer value, it will return a zero value. """ - ptr = lib.pactffi_pact_sync_http_iter_next(iter._ptr) - if ptr == ffi.NULL: - raise StopIteration - raise NotImplementedError - return SynchronousHttp(ptr) + return lib.pactffi_generator_generate_integer( + generator._ptr, + context_json.encode("utf-8"), + ) -def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: +def generators_iter_delete(iter: GeneratorCategoryIterator) -> None: """ Free the iterator when you're done using it. [Rust - `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) + `pactffi_generators_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generators_iter_delete) """ - lib.pactffi_pact_sync_http_iter_delete(iter._ptr) + lib.pactffi_generators_iter_delete(iter._ptr) -def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction: +def generators_iter_next(iter: GeneratorCategoryIterator) -> GeneratorKeyValuePair: """ - Get the next interaction from the pact. + Get the next path and generator out of the iterator, if possible. [Rust - `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_iter_next) + `pactffi_generators_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generators_iter_next) + + The returned pointer must be deleted with + `pactffi_generator_iter_pair_delete`. + + Raises: + StopIteration: + If the iterator has reached the end. """ - ptr = lib.pactffi_pact_interaction_iter_next(iter._ptr) + ptr = lib.pactffi_generators_iter_next(iter._ptr) if ptr == ffi.NULL: raise StopIteration - raise NotImplementedError - return PactInteraction(ptr) + return GeneratorKeyValuePair(ptr) -def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: +def generators_iter_pair_delete(pair: GeneratorKeyValuePair) -> None: """ - Free the iterator when you're done using it. + Free a pair of key and value returned from `pactffi_generators_iter_next`. [Rust - `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_interaction_iter_delete) + `pactffi_generators_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generators_iter_pair_delete) """ - lib.pactffi_pact_interaction_iter_delete(iter._ptr) + lib.pactffi_generators_iter_pair_delete(pair._ptr) -def matching_rule_to_json(rule: MatchingRule) -> str: +def sync_http_new() -> SynchronousHttp: """ - Get the JSON form of the matching rule. - - [Rust - `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rule_to_json) + Get a mutable pointer to a newly-created default interaction on the heap. - The returned string must be deleted with `pactffi_string_delete`. + [Rust `pactffi_sync_http_new`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_new) # Safety - This function will fail if it is passed a NULL pointer, or the iterator that - owns the value of the matching rule has been deleted. + This function is safe. + + # Error Handling + + Returns NULL on error. """ raise NotImplementedError -def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: +def sync_http_delete(interaction: SynchronousHttp) -> None: """ - Free the iterator when you're done using it. + Destroy the `SynchronousHttp` interaction being pointed to. [Rust - `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rules_iter_delete) + `pactffi_sync_http_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_delete) """ - raise NotImplementedError + lib.pactffi_sync_http_delete(interaction) -def matching_rules_iter_next( - iter: MatchingRuleCategoryIterator, -) -> MatchingRuleKeyValuePair: +def sync_http_get_request(interaction: SynchronousHttp) -> HttpRequest: """ - Get the next path and matching rule out of the iterator, if possible. + Get the request of a `SynchronousHttp` interaction. [Rust - `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rules_iter_next) - - The returned pointer must be deleted with - `pactffi_matching_rules_iter_pair_delete`. + `pactffi_sync_http_get_request`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request) # Safety - The underlying data is owned by the `MatchingRuleKeyValuePair`, so is always - safe to use. + The data pointed to by the pointer this function returns will be deleted + when the interaction is deleted. Trying to use if after the interaction is + deleted will result in undefined behaviour. # Error Handling - If no further data is present, returns NULL. + If the interaction is NULL, returns NULL. """ raise NotImplementedError -def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: +def sync_http_get_request_contents(interaction: SynchronousHttp) -> str | None: """ - Free a pair of key and value returned from `message_metadata_iter_next`. + Get the request contents of a `SynchronousHttp` interaction in string form. [Rust - `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) + `pactffi_sync_http_get_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request_contents) + + Note that this function will return `None` if either the body is missing or + is `null`. """ - raise NotImplementedError + ptr = lib.pactffi_sync_http_get_request_contents(interaction._ptr) + if ptr == ffi.NULL: + return None + return OwnedString(ptr) -def message_new() -> Message: +def sync_http_set_request_contents( + interaction: SynchronousHttp, + contents: str, + content_type: str, +) -> None: """ - Get a mutable pointer to a newly-created default message on the heap. + Sets the request contents of the interaction. [Rust - `pactffi_message_new`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_new) + `pactffi_sync_http_set_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_request_contents) + + - `interaction` - the interaction to set the request contents for + - `contents` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. # Safety - This function is safe. + The request contents and content type must either be NULL pointers, or point + to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + undefined. # Error Handling - Returns NULL on error. + If the contents is a NULL pointer, it will set the request contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. """ raise NotImplementedError -def message_new_from_json( - index: int, - json_str: str, - spec_version: PactSpecification, -) -> Message: +def sync_http_get_request_contents_length(interaction: SynchronousHttp) -> int: """ - Constructs a `Message` from the JSON string. + Get the length of the request contents of a `SynchronousHttp` interaction. [Rust - `pactffi_message_new_from_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_new_from_json) + `pactffi_sync_http_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request_contents_length) - # Safety + This function will return 0 if the body is missing. + """ + return lib.pactffi_sync_http_get_request_contents_length(interaction._ptr) - This function is safe. - # Error Handling +def sync_http_get_request_contents_bin(interaction: SynchronousHttp) -> bytes | None: + """ + Get the request contents of a `SynchronousHttp` interaction as bytes. + + [Rust + `pactffi_sync_http_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_request_contents_bin) - If the JSON string is invalid or not UTF-8 encoded, returns a NULL. + Note that this function will return `None` if either the body is missing or + is `null`. """ - raise NotImplementedError + ptr = lib.pactffi_sync_http_get_request_contents_bin(interaction._ptr) + if ptr == ffi.NULL: + return None + return ffi.buffer( + ptr, + sync_http_get_request_contents_length(interaction), + )[:] -def message_new_from_body(body: str, content_type: str) -> Message: +def sync_http_set_request_contents_bin( + interaction: SynchronousHttp, + contents: str, + len: int, + content_type: str, +) -> None: """ - Constructs a `Message` from a body with a given content-type. + Sets the request contents of the interaction as an array of bytes. [Rust - `pactffi_message_new_from_body`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_new_from_body) + `pactffi_sync_http_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_request_contents_bin) + + - `interaction` - the interaction to set the request contents for + - `contents` - pointer to contents to copy from + - `len` - number of bytes to copy from the contents pointer + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. # Safety - This function is safe. + The contents pointer must be valid for reads of `len` bytes, and it must be + properly aligned and consecutive. Otherwise behaviour is undefined. # Error Handling - If the body or content type are invalid or not UTF-8 encoded, returns NULL. + If the contents is a NULL pointer, it will set the request contents as null. + If the content type is a null pointer, or can't be parsed, it will set the + content type as unknown. """ raise NotImplementedError -def message_delete(message: Message) -> None: +def sync_http_get_response(interaction: SynchronousHttp) -> HttpResponse: """ - Destroy the `Message` being pointed to. + Get the response of a `SynchronousHttp` interaction. [Rust - `pactffi_message_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_delete) - """ - raise NotImplementedError + `pactffi_sync_http_get_response`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response) + # Safety -def message_get_contents(message: Message) -> OwnedString | None: - """ - Get the contents of a `Message` in string form. + The data pointed to by the pointer this function returns will be deleted + when the interaction is deleted. Trying to use if after the interaction is + deleted will result in undefined behaviour. - [Rust - `pactffi_message_get_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_contents) + # Error Handling - # Safety + If the interaction is NULL, returns NULL. + """ + raise NotImplementedError - The returned string must be deleted with `pactffi_string_delete` and can - outlive the message. This function must only ever be called from a foreign - language. Calling it from a Rust function that has a Tokio runtime in its - call stack can result in a deadlock. - The returned string can outlive the message. +def sync_http_get_response_contents(interaction: SynchronousHttp) -> str | None: + """ + Get the response contents of a `SynchronousHttp` interaction in string form. - # Error Handling + [Rust + `pactffi_sync_http_get_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response_contents) - If the message is NULL, returns NULL. If the body of the message is missing, - then this function also returns NULL. This means there's no mechanism to - differentiate with this function call alone between a NULL message and a - missing message body. + Note that this function will return `None` if either the body is missing or + is `null`. """ - raise NotImplementedError + ptr = lib.pactffi_sync_http_get_response_contents(interaction._ptr) + if ptr == ffi.NULL: + return None + return OwnedString(ptr) -def message_set_contents(message: Message, contents: str, content_type: str) -> None: +def sync_http_set_response_contents( + interaction: SynchronousHttp, + contents: str, + content_type: str, +) -> None: """ - Sets the contents of the message. + Sets the response contents of the interaction. [Rust - `pactffi_message_set_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_set_contents) + `pactffi_sync_http_set_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_response_contents) + + - `interaction` - the interaction to set the response contents for + - `contents` - pointer to contents to copy from. Must be a valid + NULL-terminated UTF-8 string pointer. + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. # Safety - The message contents and content type must either be NULL pointers, or point - to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is + The response contents and content type must either be NULL pointers, or + point to valid UTF-8 encoded NULL-terminated strings. Otherwise behaviour is undefined. # Error Handling - If the contents is a NULL pointer, it will set the message contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. + If the contents is a NULL pointer, it will set the response contents as + null. If the content type is a null pointer, or can't be parsed, it will set + the content type as unknown. """ raise NotImplementedError -def message_get_contents_length(message: Message) -> int: +def sync_http_get_response_contents_length(interaction: SynchronousHttp) -> int: """ - Get the length of the contents of a `Message`. + Get the length of the response contents of a `SynchronousHttp` interaction. [Rust - `pactffi_message_get_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_contents_length) - - # Safety - - This function is safe. - - # Error Handling + `pactffi_sync_http_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response_contents_length) - If the message is NULL, returns 0. If the body of the message is missing, - then this function also returns 0. + This function will return 0 if the body is missing. """ - raise NotImplementedError + return lib.pactffi_sync_http_get_response_contents_length(interaction._ptr) -def message_get_contents_bin(message: Message) -> str: +def sync_http_get_response_contents_bin(interaction: SynchronousHttp) -> bytes | None: """ - Get the contents of a `Message` as a pointer to an array of bytes. + Get the response contents of a `SynchronousHttp` interaction as bytes. [Rust - `pactffi_message_get_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_contents_bin) - - # Safety + `pactffi_sync_http_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_response_contents_bin) - The number of bytes in the buffer will be returned by - `pactffi_message_get_contents_length`. It is safe to use the pointer while - the message is not deleted or changed. Using the pointer after the message - is mutated or deleted may lead to undefined behaviour. - - # Error Handling - - If the message is NULL, returns NULL. If the body of the message is missing, - then this function also returns NULL. + Note that this function will return `None` if either the body is missing or + is `null`. """ - raise NotImplementedError + ptr = lib.pactffi_sync_http_get_response_contents_bin(interaction._ptr) + if ptr == ffi.NULL: + return None + return ffi.buffer( + ptr, + sync_http_get_response_contents_length(interaction), + )[:] -def message_set_contents_bin( - message: Message, +def sync_http_set_response_contents_bin( + interaction: SynchronousHttp, contents: str, len: int, content_type: str, ) -> None: """ - Sets the contents of the message as an array of bytes. + Sets the response contents of the `SynchronousHttp` interaction as bytes. [Rust - `pactffi_message_set_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_set_contents_bin) + `pactffi_sync_http_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_response_contents_bin) + + - `interaction` - the interaction to set the response contents for + - `contents` - pointer to contents to copy from + - `len` - number of bytes to copy + - `content_type` - pointer to the NULL-terminated UTF-8 string containing + the content type of the data. # Safety @@ -3346,42 +3767,37 @@ def message_set_contents_bin( # Error Handling - If the contents is a NULL pointer, it will set the message contents as null. - If the content type is a null pointer, or can't be parsed, it will set the - content type as unknown. + If the contents is a NULL pointer, it will set the response contents as + null. If the content type is a null pointer, or can't be parsed, it will set + the content type as unknown. """ raise NotImplementedError -def message_get_description(message: Message) -> OwnedString: +def sync_http_get_description(interaction: SynchronousHttp) -> str: r""" Get a copy of the description. [Rust - `pactffi_message_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_description) - - # Safety - - The returned string must be deleted with `pactffi_string_delete`. - - Since it is a copy, the returned string may safely outlive the `Message`. - - # Errors - - On failure, this function will return a NULL pointer. + `pactffi_sync_http_get_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_description) - This function may fail if the Rust string contains embedded null ('\0') - bytes. + Raises: + RuntimeError: + If the description cannot be retrieved """ - raise NotImplementedError + ptr = lib.pactffi_sync_http_get_description(interaction._ptr) + if ptr == ffi.NULL: + msg = "Failed to get description" + raise RuntimeError(msg) + return OwnedString(ptr) -def message_set_description(message: Message, description: str) -> int: +def sync_http_set_description(interaction: SynchronousHttp, description: str) -> int: """ - Write the `description` field on the `Message`. + Write the `description` field on the `SynchronousHttp`. [Rust - `pactffi_message_set_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_set_description) + `pactffi_sync_http_set_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_set_description) # Safety @@ -3398,18 +3814,22 @@ def message_set_description(message: Message, description: str) -> int: raise NotImplementedError -def message_get_provider_state(message: Message, index: int) -> ProviderState: +def sync_http_get_provider_state( + interaction: SynchronousHttp, + index: int, +) -> ProviderState: r""" - Get a copy of the provider state at the given index from this message. + Get a copy of the provider state at the given index from this interaction. [Rust - `pactffi_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_provider_state) + `pactffi_sync_http_get_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_provider_state) # Safety The returned structure must be deleted with `provider_state_delete`. - Since it is a copy, the returned structure may safely outlive the `Message`. + Since it is a copy, the returned structure may safely outlive the + `SynchronousHttp`. # Error Handling @@ -3421,390 +3841,344 @@ def message_get_provider_state(message: Message, index: int) -> ProviderState: raise NotImplementedError -def message_get_provider_state_iter(message: Message) -> ProviderStateIterator: +def sync_http_get_provider_state_iter( + interaction: SynchronousHttp, +) -> ProviderStateIterator: """ Get an iterator over provider states. [Rust - `pactffi_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_provider_state_iter) + `pactffi_sync_http_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_http_get_provider_state_iter) # Safety The underlying data must not change during iteration. - # Error Handling - - Returns NULL if an error occurs. - """ - raise NotImplementedError - - -def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: + Raises: + RuntimeError: + If the iterator cannot be retrieved """ - Get the next value from the iterator. - - [Rust - `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_iter_next) - - # Safety - - The underlying data must not change during iteration. + ptr = lib.pactffi_sync_http_get_provider_state_iter(interaction._ptr) + if ptr == ffi.NULL: + msg = "Failed to get provider state iterator" + raise RuntimeError(msg) + return ProviderStateIterator(ptr) - If a previous call panicked, then the internal mutex will have been poisoned - and this function will return NULL. - # Error Handling +def pact_interaction_as_synchronous_http( + interaction: PactInteraction, +) -> SynchronousHttp: + r""" + Casts this interaction to a `SynchronousHttp` interaction. - Returns NULL if an error occurs. - """ - raise NotImplementedError + Returns a NULL pointer if the interaction can not be casted to a + `SynchronousHttp` interaction (for instance, it is a message interaction). + The returned pointer must be freed with `pactffi_sync_http_delete` when no + longer required. + [Rust + `pactffi_pact_interaction_as_synchronous_http`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_http) -def provider_state_iter_delete(iter: ProviderStateIterator) -> None: - """ - Delete the iterator. + # Safety This function is safe as long as the interaction pointer is a valid + pointer. - [Rust - `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_iter_delete) + # Errors On any error, this function will return a NULL pointer. """ raise NotImplementedError -def message_find_metadata(message: Message, key: str) -> str: - r""" - Get a copy of the metadata value indexed by `key`. - - [Rust - `pactffi_message_find_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_find_metadata) - - # Safety - - The returned string must be deleted with `pactffi_string_delete`. - - Since it is a copy, the returned string may safely outlive the `Message`. - - The returned pointer will be NULL if the metadata does not contain the given - key, or if an error occurred. - - # Error Handling - - On failure, this function will return a NULL pointer. - - This function may fail if the provided `key` string contains invalid UTF-8, - or if the Rust string contains embedded null ('\0') bytes. +def pact_interaction_as_asynchronous_message( + interaction: PactInteraction, +) -> AsynchronousMessage: """ - raise NotImplementedError - + Casts this interaction to a `AsynchronousMessage` interaction. -def message_insert_metadata(message: Message, key: str, value: str) -> int: - r""" - Insert the (`key`, `value`) pair into this Message's `metadata` HashMap. + Returns a NULL pointer if the interaction can not be casted to a + `AsynchronousMessage` interaction (for instance, it is a http interaction). + The returned pointer must be freed with `pactffi_async_message_delete` when + no longer required. [Rust - `pactffi_message_insert_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_insert_metadata) - - # Safety + `pactffi_pact_interaction_as_asynchronous_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_as_asynchronous_message) - This function returns an enum indicating the result; see the comments on - HashMapInsertStatus for details. + Note that if the interaction is a V3 `Message`, it will be converted to a V4 + `AsynchronousMessage` before being returned. - # Error Handling + # Safety This function is safe as long as the interaction pointer is a valid + pointer. - This function may fail if the provided `key` or `value` strings contain - invalid UTF-8. + # Errors On any error, this function will return a NULL pointer. """ raise NotImplementedError -def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadataPair: - """ - Get the next key and value out of the iterator, if possible. - - [Rust - `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_metadata_iter_next) - - The returned pointer must be deleted with - `pactffi_message_metadata_pair_delete`. - - # Safety - - The underlying data must not change during iteration. This function must - only ever be called from a foreign language. Calling it from a Rust function - that has a Tokio runtime in its call stack can result in a deadlock. - - # Error Handling - - If no further data is present, returns NULL. +def pact_interaction_as_synchronous_message( + interaction: PactInteraction, +) -> SynchronousMessage: """ - raise NotImplementedError - + Casts this interaction to a `SynchronousMessage` interaction. -def message_get_metadata_iter(message: Message) -> MessageMetadataIterator: - r""" - Get an iterator over the metadata of a message. + Returns a NULL pointer if the interaction can not be casted to a + `SynchronousMessage` interaction (for instance, it is a http interaction). + The returned pointer must be freed with `pactffi_sync_message_delete` when + no longer required. [Rust - `pactffi_message_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_get_metadata_iter) - - # Safety - - This iterator carries a pointer to the message, and must not outlive the - message. - - The message metadata also must not be modified during iteration. If it is, - the old iterator must be deleted and a new iterator created. - - # Error Handling + `pactffi_pact_interaction_as_synchronous_message`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_as_synchronous_message) - On failure, this function will return a NULL pointer. + # Safety This function is safe as long as the interaction pointer is a valid + pointer. - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. + # Errors On any error, this function will return a NULL pointer. """ raise NotImplementedError -def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: +def pact_async_message_iter_next(iter: PactAsyncMessageIterator) -> AsynchronousMessage: """ - Free the metadata iterator when you're done using it. + Get the next asynchronous message from the iterator. [Rust - `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_metadata_iter_delete) + `pactffi_pact_async_message_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_async_message_iter_next) + + Raises: + StopIteration: + If the iterator has reached the end. """ - raise NotImplementedError + ptr = lib.pactffi_pact_async_message_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + return AsynchronousMessage(ptr, owned=True) -def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: +def pact_async_message_iter_delete(iter: PactAsyncMessageIterator) -> None: """ - Free a pair of key and value returned from `message_metadata_iter_next`. + Free the iterator when you're done using it. [Rust - `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_metadata_pair_delete) + `pactffi_pact_async_message_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_async_message_iter_delete) """ - raise NotImplementedError + lib.pactffi_pact_async_message_iter_delete(iter._ptr) -def message_pact_new_from_json(file_name: str, json_str: str) -> MessagePact: +def pact_sync_message_iter_next(iter: PactSyncMessageIterator) -> SynchronousMessage: """ - Construct a new `MessagePact` from the JSON string. - - The provided file name is used when generating error messages. + Get the next synchronous request/response message from the V4 pact. [Rust - `pactffi_message_pact_new_from_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_new_from_json) - - # Safety - - The `file_name` and `json_str` parameters must both be valid UTF-8 encoded - strings. + `pactffi_pact_sync_message_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_message_iter_next) - # Error Handling - - On error, this function will return a null pointer. + Raises: + StopIteration: + If the iterator has reached the end. """ - raise NotImplementedError + ptr = lib.pactffi_pact_sync_message_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + return SynchronousMessage(ptr, owned=True) -def message_pact_delete(message_pact: MessagePact) -> None: +def pact_sync_message_iter_delete(iter: PactSyncMessageIterator) -> None: """ - Delete the `MessagePact` being pointed to. + Free the iterator when you're done using it. [Rust - `pactffi_message_pact_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_delete) + `pactffi_pact_sync_message_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_message_iter_delete) """ - raise NotImplementedError + lib.pactffi_pact_sync_message_iter_delete(iter._ptr) -def message_pact_get_consumer(message_pact: MessagePact) -> Consumer: +def pact_sync_http_iter_next(iter: PactSyncHttpIterator) -> SynchronousHttp: """ - Get a pointer to the Consumer struct inside the MessagePact. - - This is a mutable borrow: The caller may mutate the Consumer through this - pointer. + Get the next synchronous HTTP request/response interaction from the V4 pact. [Rust - `pactffi_message_pact_get_consumer`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_get_consumer) - - # Safety - - This function is safe. - - # Error Handling + `pactffi_pact_sync_http_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_http_iter_next) - This function will only fail if it is passed a NULL pointer. In the case of - error, a NULL pointer will be returned. + Raises: + StopIteration: + If the iterator has reached the end. """ - raise NotImplementedError + ptr = lib.pactffi_pact_sync_http_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + return SynchronousHttp(ptr, owned=True) -def message_pact_get_provider(message_pact: MessagePact) -> Provider: +def pact_sync_http_iter_delete(iter: PactSyncHttpIterator) -> None: """ - Get a pointer to the Provider struct inside the MessagePact. - - This is a mutable borrow: The caller may mutate the Provider through this - pointer. + Free the iterator when you're done using it. [Rust - `pactffi_message_pact_get_provider`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_get_provider) - - # Safety - - This function is safe. - - # Error Handling - - This function will only fail if it is passed a NULL pointer. In the case of - error, a NULL pointer will be returned. + `pactffi_pact_sync_http_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_sync_http_iter_delete) """ - raise NotImplementedError - - -def message_pact_get_message_iter( - message_pact: MessagePact, -) -> MessagePactMessageIterator: - r""" - Get an iterator over the messages of a message pact. - - [Rust - `pactffi_message_pact_get_message_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_get_message_iter) - - # Safety - - This iterator carries a pointer to the message pact, and must not outlive - the message pact. + lib.pactffi_pact_sync_http_iter_delete(iter._ptr) - The message pact messages also must not be modified during iteration. If - they are, the old iterator must be deleted and a new iterator created. - # Error Handling +def pact_interaction_iter_next(iter: PactInteractionIterator) -> PactInteraction: + """ + Get the next interaction from the pact. - On failure, this function will return a NULL pointer. + [Rust + `pactffi_pact_interaction_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_iter_next) - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. + Raises: + StopIteration: + If the iterator has reached the end. """ + ptr = lib.pactffi_pact_interaction_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration raise NotImplementedError + return PactInteraction(ptr) -def message_pact_message_iter_next(iter: MessagePactMessageIterator) -> Message: +def pact_interaction_iter_delete(iter: PactInteractionIterator) -> None: """ - Get the next message from the message pact. + Free the iterator when you're done using it. [Rust - `pactffi_message_pact_message_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_message_iter_next) + `pactffi_pact_interaction_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_interaction_iter_delete) + """ + lib.pactffi_pact_interaction_iter_delete(iter._ptr) - # Safety - This function is safe. +def matching_rule_to_json(rule: MatchingRule) -> str: + """ + Get the JSON form of the matching rule. - # Error Handling + [Rust + `pactffi_matching_rule_to_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rule_to_json) - This function will return a NULL pointer if passed a NULL pointer or if an - error occurs. + The returned string must be deleted with `pactffi_string_delete`. + + # Safety + + This function will fail if it is passed a NULL pointer, or the iterator that + owns the value of the matching rule has been deleted. """ - raise NotImplementedError + return OwnedString(lib.pactffi_matching_rule_to_json(rule._ptr)) -def message_pact_message_iter_delete(iter: MessagePactMessageIterator) -> None: +def matching_rules_iter_delete(iter: MatchingRuleCategoryIterator) -> None: """ - Delete the iterator. + Free the iterator when you're done using it. [Rust - `pactffi_message_pact_message_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_message_iter_delete) + `pactffi_matching_rules_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rules_iter_delete) """ - raise NotImplementedError + lib.pactffi_matching_rules_iter_delete(iter._ptr) -def message_pact_find_metadata(message_pact: MessagePact, key1: str, key2: str) -> str: - r""" - Get a copy of the metadata value indexed by `key1` and `key2`. +def matching_rules_iter_next( + iter: MatchingRuleCategoryIterator, +) -> MatchingRuleKeyValuePair: + """ + Get the next path and matching rule out of the iterator, if possible. [Rust - `pactffi_message_pact_find_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_find_metadata) + `pactffi_matching_rules_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rules_iter_next) + + The returned pointer must be deleted with + `pactffi_matching_rules_iter_pair_delete`. # Safety - Since it is a copy, the returned string may safely outlive the `Message`. + The underlying data is owned by the `MatchingRuleKeyValuePair`, so is always + safe to use. - The returned string must be deleted with `pactffi_string_delete`. + # Error Handling - The returned pointer will be NULL if the metadata does not contain the given - key, or if an error occurred. + If no further data is present, returns NULL. + """ + return MatchingRuleKeyValuePair(lib.pactffi_matching_rules_iter_next(iter._ptr)) - # Error Handling - On failure, this function will return a NULL pointer. +def matching_rules_iter_pair_delete(pair: MatchingRuleKeyValuePair) -> None: + """ + Free a pair of key and value returned from `message_metadata_iter_next`. - This function may fail if the provided `key1` or `key2` strings contains - invalid UTF-8, or if the Rust string contains embedded null ('\0') bytes. + [Rust + `pactffi_matching_rules_iter_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matching_rules_iter_pair_delete) """ - raise NotImplementedError + lib.pactffi_matching_rules_iter_pair_delete(pair._ptr) -def message_pact_get_metadata_iter( - message_pact: MessagePact, -) -> MessagePactMetadataIterator: - r""" - Get an iterator over the metadata of a message pact. +def provider_state_iter_next(iter: ProviderStateIterator) -> ProviderState: + """ + Get the next value from the iterator. [Rust - `pactffi_message_pact_get_metadata_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_get_metadata_iter) + `pactffi_provider_state_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_iter_next) # Safety - This iterator carries a pointer to the message pact, and must not outlive - the message pact. + The underlying data must not change during iteration. - The message pact metadata also must not be modified during iteration. If it - is, the old iterator must be deleted and a new iterator created. + Raises: + StopIteration: + If no further data is present, or if an internal error occurs. + """ + provider_state = lib.pactffi_provider_state_iter_next(iter._ptr) + if provider_state == ffi.NULL: + raise StopIteration + return ProviderState(provider_state) - # Error Handling - On failure, this function will return a NULL pointer. +def provider_state_iter_delete(iter: ProviderStateIterator) -> None: + """ + Delete the iterator. - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. + [Rust + `pactffi_provider_state_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_iter_delete) """ - raise NotImplementedError + lib.pactffi_provider_state_iter_delete(iter._ptr) -def message_pact_metadata_iter_next( - iter: MessagePactMetadataIterator, -) -> MessagePactMetadataTriple: +def message_metadata_iter_next(iter: MessageMetadataIterator) -> MessageMetadataPair: """ - Get the next triple out of the iterator, if possible. + Get the next key and value out of the iterator, if possible. [Rust - `pactffi_message_pact_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_metadata_iter_next) + `pactffi_message_metadata_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_metadata_iter_next) - # Safety + The returned pointer must be deleted with + `pactffi_message_metadata_pair_delete`. - This operation is invalid if the underlying data has been changed during - iteration. + # Safety - # Error Handling + The underlying data must not change during iteration. This function must + only ever be called from a foreign language. Calling it from a Rust function + that has a Tokio runtime in its call stack can result in a deadlock. - Returns null if no next element is present. + Raises: + StopIteration: + If no further data is present. """ - raise NotImplementedError + ptr = lib.pactffi_message_metadata_iter_next(iter._ptr) + if ptr == ffi.NULL: + raise StopIteration + return MessageMetadataPair(ptr) -def message_pact_metadata_iter_delete(iter: MessagePactMetadataIterator) -> None: +def message_metadata_iter_delete(iter: MessageMetadataIterator) -> None: """ Free the metadata iterator when you're done using it. - [Rust `pactffi_message_pact_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_metadata_iter_delete) + [Rust + `pactffi_message_metadata_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_metadata_iter_delete) """ - raise NotImplementedError + lib.pactffi_message_metadata_iter_delete(iter._ptr) -def message_pact_metadata_triple_delete(triple: MessagePactMetadataTriple) -> None: +def message_metadata_pair_delete(pair: MessageMetadataPair) -> None: """ - Free a triple returned from `pactffi_message_pact_metadata_iter_next`. + Free a pair of key and value returned from `message_metadata_iter_next`. - [Rust `pactffi_message_pact_metadata_triple_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_pact_metadata_triple_delete) + [Rust + `pactffi_message_metadata_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_message_metadata_pair_delete) """ - raise NotImplementedError + lib.pactffi_message_metadata_pair_delete(pair._ptr) def provider_get_name(provider: Provider) -> str: @@ -3812,7 +4186,7 @@ def provider_get_name(provider: Provider) -> str: Get a copy of this provider's name. [Rust - `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_get_name) + `pactffi_provider_get_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_get_name) The copy must be deleted with `pactffi_string_delete`. @@ -3858,7 +4232,7 @@ def pact_get_provider(pact: Pact) -> Provider: `pactffi_pact_provider_delete` when no longer required. [Rust - `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_get_provider) + `pactffi_pact_get_provider`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_get_provider) # Errors @@ -3873,29 +4247,27 @@ def pact_provider_delete(provider: Provider) -> None: Frees the memory used by the Pact provider. [Rust - `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_provider_delete) + `pactffi_pact_provider_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_provider_delete) """ raise NotImplementedError -def provider_state_get_name(provider_state: ProviderState) -> str: +def provider_state_get_name(provider_state: ProviderState) -> str | None: """ Get the name of the provider state as a string. - This needs to be deleted with `pactffi_string_delete`. - [Rust - `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_get_name) - - # Safety - - This function is safe. + `pactffi_provider_state_get_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_get_name) - # Error Handling - - If the provider_state param is NULL, this returns NULL. + Raises: + RuntimeError: + If the name could not be retrieved. """ - raise NotImplementedError + ptr = lib.pactffi_provider_state_get_name(provider_state._ptr) + if ptr == ffi.NULL: + msg = "Failed to get provider state name." + raise RuntimeError(msg) + return OwnedString(ptr) def provider_state_get_param_iter( @@ -3905,7 +4277,7 @@ def provider_state_get_param_iter( Get an iterator over the params of a provider state. [Rust - `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_get_param_iter) + `pactffi_provider_state_get_param_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_get_param_iter) # Safety @@ -3915,14 +4287,15 @@ def provider_state_get_param_iter( The provider state params also must not be modified during iteration. If it is, the old iterator must be deleted and a new iterator created. - # Errors - - On failure, this function will return a NULL pointer. - - This function may fail if any of the Rust strings contain embedded null - ('\0') bytes. + Raises: + RuntimeError: + If the iterator could not be created. """ - raise NotImplementedError + ptr = lib.pactffi_provider_state_get_param_iter(provider_state._ptr) + if ptr == ffi.NULL: + msg = "Failed to get provider state param iterator." + raise RuntimeError(msg) + return ProviderStateParamIterator(ptr) def provider_state_param_iter_next( @@ -3932,22 +4305,20 @@ def provider_state_param_iter_next( Get the next key and value out of the iterator, if possible. [Rust - `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_param_iter_next) - - Returns a pointer to a heap allocated array of 2 elements, the pointer to - the key string on the heap, and the pointer to the value string on the heap. + `pactffi_provider_state_param_iter_next`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_param_iter_next) # Safety The underlying data must not be modified during iteration. - The user needs to free both the contained strings and the array. - - # Error Handling - - Returns NULL if there's no further elements or the iterator is NULL. + Raises: + StopIteration: + If no further data is present. """ - raise NotImplementedError + provider_state_param = lib.pactffi_provider_state_param_iter_next(iter._ptr) + if provider_state_param == ffi.NULL: + raise StopIteration + return ProviderStateParamPair(provider_state_param) def provider_state_delete(provider_state: ProviderState) -> None: @@ -3955,7 +4326,7 @@ def provider_state_delete(provider_state: ProviderState) -> None: Free the provider state when you're done using it. [Rust - `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_delete) + `pactffi_provider_state_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_delete) """ raise NotImplementedError @@ -3965,9 +4336,9 @@ def provider_state_param_iter_delete(iter: ProviderStateParamIterator) -> None: Free the provider state param iterator when you're done using it. [Rust - `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_param_iter_delete) + `pactffi_provider_state_param_iter_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_param_iter_delete) """ - raise NotImplementedError + lib.pactffi_provider_state_param_iter_delete(iter._ptr) def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: @@ -3975,9 +4346,9 @@ def provider_state_param_pair_delete(pair: ProviderStateParamPair) -> None: Free a pair of key and value. [Rust - `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_provider_state_param_pair_delete) + `pactffi_provider_state_param_pair_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_provider_state_param_pair_delete) """ - raise NotImplementedError + lib.pactffi_provider_state_param_pair_delete(pair._ptr) def sync_message_new() -> SynchronousMessage: @@ -3985,7 +4356,7 @@ def sync_message_new() -> SynchronousMessage: Get a mutable pointer to a newly-created default message on the heap. [Rust - `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_new) + `pactffi_sync_message_new`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_new) # Safety @@ -4003,9 +4374,9 @@ def sync_message_delete(message: SynchronousMessage) -> None: Destroy the `Message` being pointed to. [Rust - `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_delete) + `pactffi_sync_message_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_delete) """ - raise NotImplementedError + lib.pactffi_sync_message_delete(message._ptr) def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: @@ -4013,7 +4384,7 @@ def sync_message_get_request_contents_str(message: SynchronousMessage) -> str: Get the request contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) + `pactffi_sync_message_get_request_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents_str) # Safety @@ -4040,7 +4411,7 @@ def sync_message_set_request_contents_str( Sets the request contents of the message. [Rust - `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) + `pactffi_sync_message_set_request_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_request_contents_str) - `message` - the message to set the request contents for - `contents` - pointer to contents to copy from. Must be a valid @@ -4068,7 +4439,7 @@ def sync_message_get_request_contents_length(message: SynchronousMessage) -> int Get the length of the request contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) + `pactffi_sync_message_get_request_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents_length) # Safety @@ -4087,7 +4458,7 @@ def sync_message_get_request_contents_bin(message: SynchronousMessage) -> bytes: Get the request contents of a `SynchronousMessage` as a bytes. [Rust - `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) + `pactffi_sync_message_get_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents_bin) # Safety @@ -4114,7 +4485,7 @@ def sync_message_set_request_contents_bin( Sets the request contents of the message as an array of bytes. [Rust - `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) + `pactffi_sync_message_set_request_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_request_contents_bin) * `message` - the message to set the request contents for * `contents` - pointer to contents to copy from @@ -4141,7 +4512,7 @@ def sync_message_get_request_contents(message: SynchronousMessage) -> MessageCon Get the request contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_request_contents) + `pactffi_sync_message_get_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_request_contents) # Safety @@ -4156,22 +4527,41 @@ def sync_message_get_request_contents(message: SynchronousMessage) -> MessageCon raise NotImplementedError -def sync_message_get_number_responses(message: SynchronousMessage) -> int: +def sync_message_generate_request_contents( + message: SynchronousMessage, +) -> MessageContents: """ - Get the number of response messages in the `SynchronousMessage`. + Get the request contents of an `SynchronousMessage` as a `MessageContents`. + + This function differs from `pactffi_sync_message_get_request_contents` in + that it will process the message contents for any generators or matchers + that are present in the message in order to generate the actual message + contents as would be received by the consumer. [Rust - `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_number_responses) + `pactffi_sync_message_generate_request_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_generate_request_contents) - # Safety + Raises: + RuntimeError: + If the request contents cannot be generated + """ + ptr = lib.pactffi_sync_message_generate_request_contents(message._ptr) + if ptr == ffi.NULL: + msg = "Failed to generate request contents" + raise RuntimeError(msg) + return MessageContents(ptr, owned=False) - The message pointer must point to a valid SynchronousMessage. - # Error Handling +def sync_message_get_number_responses(message: SynchronousMessage) -> int: + """ + Get the number of response messages in the `SynchronousMessage`. + + [Rust + `pactffi_sync_message_get_number_responses`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_number_responses) - If the message is NULL, returns 0. + If the message is null, this function will return 0. """ - raise NotImplementedError + return lib.pactffi_sync_message_get_number_responses(message._ptr) def sync_message_get_response_contents_str( @@ -4182,7 +4572,7 @@ def sync_message_get_response_contents_str( Get the response contents of a `SynchronousMessage` in string form. [Rust - `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) + `pactffi_sync_message_get_response_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents_str) # Safety @@ -4215,7 +4605,7 @@ def sync_message_set_response_contents_str( with default values. [Rust - `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) + `pactffi_sync_message_set_response_contents_str`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_response_contents_str) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response. @@ -4247,7 +4637,7 @@ def sync_message_get_response_contents_length( Get the length of the response contents of a `SynchronousMessage`. [Rust - `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) + `pactffi_sync_message_get_response_contents_length`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents_length) # Safety @@ -4269,7 +4659,7 @@ def sync_message_get_response_contents_bin( Get the response contents of a `SynchronousMessage` as bytes. [Rust - `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) + `pactffi_sync_message_get_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents_bin) # Safety @@ -4300,7 +4690,7 @@ def sync_message_set_response_contents_bin( responses will be padded with default values. [Rust - `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) + `pactffi_sync_message_set_response_contents_bin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_response_contents_bin) * `message` - the message to set the response contents for * `index` - index of the response to set. 0 is the first response @@ -4331,7 +4721,7 @@ def sync_message_get_response_contents( Get the response contents of an `SynchronousMessage` as a `MessageContents`. [Rust - `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_response_contents) + `pactffi_sync_message_get_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_response_contents) # Safety @@ -4346,28 +4736,49 @@ def sync_message_get_response_contents( raise NotImplementedError -def sync_message_get_description(message: SynchronousMessage) -> str: - r""" - Get a copy of the description. +def sync_message_generate_response_contents( + message: SynchronousMessage, + index: int, +) -> MessageContents: + """ + Get the response contents of an `SynchronousMessage` as a `MessageContents`. - [Rust - `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_description) + This function differs from + `sync_message_get_response_contents` in that it will process + the message contents for any generators or matchers that are present in + the message in order to generate the actual message contents as would be + received by the consumer. - # Safety + [Rust + `pactffi_sync_message_generate_response_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_generate_response_contents) - The returned string must be deleted with `pactffi_string_delete`. + Raises: + RuntimeError: + If the response contents could not be generated. + """ + ptr = lib.pactffi_sync_message_generate_response_contents(message._ptr, index) + if ptr == ffi.NULL: + msg = "Failed to generate response contents." + raise RuntimeError(msg) + return MessageContents(ptr, owned=False) - Since it is a copy, the returned string may safely outlive the - `SynchronousMessage`. - # Errors +def sync_message_get_description(message: SynchronousMessage) -> str: + r""" + Get a copy of the description. - On failure, this function will return a NULL pointer. + [Rust + `pactffi_sync_message_get_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_description) - This function may fail if the Rust string contains embedded null ('\0') - bytes. + Raises: + RuntimeError: + If the description could not be retrieved """ - raise NotImplementedError + ptr = lib.pactffi_sync_message_get_description(message._ptr) + if ptr == ffi.NULL: + msg = "Failed to get description." + raise RuntimeError(msg) + return OwnedString(ptr) def sync_message_set_description(message: SynchronousMessage, description: str) -> int: @@ -4375,7 +4786,7 @@ def sync_message_set_description(message: SynchronousMessage, description: str) Write the `description` field on the `SynchronousMessage`. [Rust - `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_set_description) + `pactffi_sync_message_set_description`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_set_description) # Safety @@ -4400,7 +4811,7 @@ def sync_message_get_provider_state( Get a copy of the provider state at the given index from this message. [Rust - `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_provider_state) + `pactffi_sync_message_get_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_provider_state) # Safety @@ -4426,17 +4837,21 @@ def sync_message_get_provider_state_iter( Get an iterator over provider states. [Rust - `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) + `pactffi_sync_message_get_provider_state_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_sync_message_get_provider_state_iter) # Safety The underlying data must not change during iteration. - # Error Handling - - Returns NULL if an error occurs. + Raises: + RuntimeError: + If the iterator could not be created. """ - raise NotImplementedError + ptr = lib.pactffi_sync_message_get_provider_state_iter(message._ptr) + if ptr == ffi.NULL: + msg = "Failed to get provider state iterator." + raise RuntimeError(msg) + return ProviderStateIterator(ptr) def string_delete(string: OwnedString) -> None: @@ -4444,7 +4859,7 @@ def string_delete(string: OwnedString) -> None: Delete a string previously returned by this FFI. [Rust - `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_string_delete) + `pactffi_string_delete`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_string_delete) """ lib.pactffi_string_delete(string._ptr) @@ -4459,7 +4874,7 @@ def create_mock_server(pact_str: str, addr_str: str, *, tls: bool) -> int: the mock server is returned. [Rust - `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_create_mock_server) + `pactffi_create_mock_server`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_create_mock_server) * `pact_str` - Pact JSON * `addr_str` - Address to bind to in the form name:port (i.e. 127.0.0.1:80) @@ -4495,7 +4910,7 @@ def get_tls_ca_certificate() -> OwnedString: Fetch the CA Certificate used to generate the self-signed certificate. [Rust - `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_get_tls_ca_certificate) + `pactffi_get_tls_ca_certificate`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_get_tls_ca_certificate) **NOTE:** The string for the result is allocated on the heap, and will have to be freed by the caller using pactffi_string_delete. @@ -4516,7 +4931,7 @@ def create_mock_server_for_pact(pact: PactHandle, addr_str: str, *, tls: bool) - operating system. The port of the mock server is returned. [Rust - `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_create_mock_server_for_pact) + `pactffi_create_mock_server_for_pact`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_create_mock_server_for_pact) * `pact` - Handle to a Pact model created with created with `pactffi_new_pact`. @@ -4559,7 +4974,7 @@ def create_mock_server_for_transport( Create a mock server for the provided Pact handle and transport. [Rust - `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_create_mock_server_for_transport) + `pactffi_create_mock_server_for_transport`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_create_mock_server_for_transport) Args: pact: @@ -4584,8 +4999,9 @@ def create_mock_server_for_transport( A handle to the mock server. Raises: - RuntimeError: If the mock server could not be created. The error message - will contain details of the error. + RuntimeError: + If the mock server could not be created. The error message will + contain details of the error. """ ret: int = lib.pactffi_create_mock_server_for_transport( pact._ref, @@ -4620,7 +5036,7 @@ def mock_server_matched(mock_server_handle: PactServerHandle) -> bool: if any request has not been successfully matched, or the method panics. [Rust - `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mock_server_matched) + `pactffi_mock_server_matched`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mock_server_matched) """ return lib.pactffi_mock_server_matched(mock_server_handle._ref) @@ -4632,13 +5048,14 @@ def mock_server_mismatches( External interface to get all the mismatches from a mock server. [Rust - `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mock_server_mismatches) + `pactffi_mock_server_mismatches`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mock_server_mismatches) # Errors Raises: - RuntimeError: If there is no mock server with the provided port number, - or the function panics. + RuntimeError: + If there is no mock server with the provided port number, or the + function panics. """ ptr = lib.pactffi_mock_server_mismatches(mock_server_handle._ref) if ptr == ffi.NULL: @@ -4658,14 +5075,15 @@ def cleanup_mock_server(mock_server_handle: PactServerHandle) -> None: and cleanup any memory allocated for it. [Rust - `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_cleanup_mock_server) + `pactffi_cleanup_mock_server`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_cleanup_mock_server) Args: mock_server_handle: Handle to the mock server to cleanup. Raises: - RuntimeError: If the mock server could not be cleaned up. + RuntimeError: + If the mock server could not be cleaned up. """ success: bool = lib.pactffi_cleanup_mock_server(mock_server_handle._ref) if not success: @@ -4686,7 +5104,7 @@ def write_pact_file( directory to write the file to is passed as the second parameter. [Rust - `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_write_pact_file) + `pactffi_write_pact_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_write_pact_file) Args: mock_server_handle: @@ -4700,7 +5118,8 @@ def write_pact_file( pact file will be merged with any existing pact file. Raises: - RuntimeError: If there was an error writing the pact file. + RuntimeError: + If there was an error writing the pact file. """ ret: int = lib.pactffi_write_pact_file( mock_server_handle._ref, @@ -4737,10 +5156,11 @@ def mock_server_logs(mock_server_handle: PactServerHandle) -> str: started. [Rust - `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_mock_server_logs) + `pactffi_mock_server_logs`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_mock_server_logs) Raises: - RuntimeError: If the logs for the mock server can not be retrieved. + RuntimeError: + If the logs for the mock server can not be retrieved. """ ptr = lib.pactffi_mock_server_logs(mock_server_handle._ref) if ptr == ffi.NULL: @@ -4760,7 +5180,7 @@ def generate_datetime_string(format: str) -> StringResult: string needs to be freed with the `pactffi_string_delete` function [Rust - `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generate_datetime_string) + `pactffi_generate_datetime_string`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generate_datetime_string) # Safety @@ -4777,7 +5197,7 @@ def check_regex(regex: str, example: str) -> bool: Checks that the example string matches the given regex. [Rust - `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_check_regex) + `pactffi_check_regex`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_check_regex) # Safety @@ -4796,7 +5216,7 @@ def generate_regex_value(regex: str) -> StringResult: `pactffi_string_delete` function. [Rust - `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_generate_regex_value) + `pactffi_generate_regex_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_generate_regex_value) # Safety @@ -4811,7 +5231,7 @@ def free_string(s: str) -> None: [DEPRECATED] Frees the memory allocated to a string by another function. [Rust - `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_free_string) + `pactffi_free_string`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_free_string) This function is deprecated. Use `pactffi_string_delete` instead. @@ -4833,7 +5253,7 @@ def new_pact(consumer_name: str, provider_name: str) -> PactHandle: Creates a new Pact model and returns a handle to it. [Rust - `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_pact) + `pactffi_new_pact`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_pact) Args: consumer_name: @@ -4874,7 +5294,7 @@ def new_interaction(pact: PactHandle, description: str) -> InteractionHandle: will result in that interaction being replaced with the new one. [Rust - `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_interaction) + `pactffi_new_interaction`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_interaction) Args: pact: @@ -4902,7 +5322,7 @@ def new_message_interaction(pact: PactHandle, description: str) -> InteractionHa will result in that interaction being replaced with the new one. [Rust - `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_message_interaction) + `pactffi_new_message_interaction`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_message_interaction) Args: pact: @@ -4933,7 +5353,7 @@ def new_sync_message_interaction( will result in that interaction being replaced with the new one. [Rust - `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_sync_message_interaction) + `pactffi_new_sync_message_interaction`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_new_sync_message_interaction) Args: pact: @@ -4958,7 +5378,7 @@ def upon_receiving(interaction: InteractionHandle, description: str) -> None: Sets the description for the Interaction. [Rust - `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_upon_receiving) + `pactffi_upon_receiving`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_upon_receiving) This function @@ -4970,7 +5390,11 @@ def upon_receiving(interaction: InteractionHandle, description: str) -> None: The interaction description. It needs to be unique for each Pact. Raises: - RuntimeError: If the interaction description could not be set. + NotImplementedError: + This function has intentionally been left unimplemented. + + RuntimeError: + If the interaction description could not be set. """ # This function has intentionally been left unimplemented. The rationale is # to avoid code of the form: @@ -4995,7 +5419,7 @@ def given(interaction: InteractionHandle, description: str) -> None: Adds a provider state to the Interaction. [Rust - `pactffi_given`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_given) + `pactffi_given`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_given) Args: interaction: @@ -5005,7 +5429,8 @@ def given(interaction: InteractionHandle, description: str) -> None: The provider state description. It needs to be unique. Raises: - RuntimeError: If the provider state could not be specified. + RuntimeError: + If the provider state could not be specified. """ success: bool = lib.pactffi_given(interaction._ref, description.encode("utf-8")) if not success: @@ -5021,7 +5446,7 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> Non used with V4 interactions. [Rust - `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_interaction_test_name) + `pactffi_interaction_test_name`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_interaction_test_name) Args: interaction: @@ -5030,23 +5455,10 @@ def interaction_test_name(interaction: InteractionHandle, test_name: str) -> Non test_name: The test name to set. - # Safety - - The test name parameter must be a valid pointer to a NULL terminated string. - Raises: - RuntimeError: If the test name can not be set. - - # Error Handling - - If the test name can not be set, this will return a positive value. + RuntimeError: + If the test name can not be set. - * `1` - Function panicked. Error message will be available by calling - `pactffi_get_error_message`. - * `2` - Handle was not valid. - * `3` - Mock server was already started and the integration can not be - modified. - * `4` - Not a V4 interaction. """ ret: int = lib.pactffi_interaction_test_name( interaction._ref, @@ -5081,7 +5493,7 @@ def given_with_param( be parsed as JSON. [Rust - `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_given_with_param) + `pactffi_given_with_param`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_given_with_param) Args: interaction: @@ -5097,7 +5509,8 @@ def given_with_param( Parameter value as JSON. Raises: - RuntimeError: If the interaction state could not be updated. + RuntimeError: + If the interaction state could not be updated. """ success: bool = lib.pactffi_given_with_param( interaction._ref, @@ -5122,7 +5535,7 @@ def given_with_params( with a `value` key. [Rust - `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_given_with_params) + `pactffi_given_with_params`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_given_with_params) Args: interaction: @@ -5135,19 +5548,8 @@ def given_with_params( Parameter values as a JSON fragment. Raises: - RuntimeError: If the interaction state could not be updated. - - # Errors - - Returns EXIT_FAILURE (1) if the interaction or Pact can't be modified (i.e. - the mock server for it has already started). - - Returns 2 and sets the error message (which can be retrieved with - `pactffi_get_error_message`) if the parameter values con't be parsed as - JSON. - - Returns 3 if any of the C strings are not valid. - + RuntimeError: + If the interaction state could not be updated. """ ret: int = lib.pactffi_given_with_params( interaction._ref, @@ -5172,7 +5574,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None Configures the request for the Interaction. [Rust - `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_request) + `pactffi_with_request`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_request) Args: interaction: @@ -5186,7 +5588,7 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) which allows regex patterns. For examples: ```json @@ -5198,7 +5600,8 @@ def with_request(interaction: InteractionHandle, method: str, path: str) -> None ``` Raises: - RuntimeError: If the request could not be specified. + RuntimeError: + If the request could not be specified. """ success: bool = lib.pactffi_with_request( interaction._ref, @@ -5220,7 +5623,7 @@ def with_query_parameter_v2( Configures a query parameter for the Interaction. [Rust - `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_query_parameter_v2) + `pactffi_with_query_parameter_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_query_parameter_v2) To setup a query parameter with multiple values, you can either call this function multiple times with a different index value: @@ -5257,7 +5660,7 @@ def with_query_parameter_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) If you want the matching rules to apply to all values (and not just the one with the given index), make sure to set the value to be an array. @@ -5275,6 +5678,24 @@ def with_query_parameter_v2( ) ``` + For query parameters with no value, two distinct formats are provided: + + 1. Parameters with blank values, as specified by `?foo=&bar=`, require an + empty string: + + ```python + with_query_parameter_v2(handle, "foo", 0, "") + with_query_parameter_v2(handle, "bar", 0, "") + ``` + + 2. Parameters with no associated value, as specified by `?foo&bar`, require + a NULL pointer: + + ```python + with_query_parameter_v2(handle, "foo", 0, None) + with_query_parameter_v2(handle, "bar", 0, None) + ``` + Args: interaction: Handle to the Interaction. @@ -5291,10 +5712,11 @@ def with_query_parameter_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). Raises: - RuntimeError: If there was an error setting the query parameter. + RuntimeError: + If there was an error setting the query parameter. """ success: bool = lib.pactffi_with_query_parameter_v2( interaction._ref, @@ -5312,7 +5734,7 @@ def with_specification(pact: PactHandle, version: PactSpecification) -> None: Sets the specification version for a given Pact model. [Rust - `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_specification) + `pactffi_with_specification`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_specification) Args: pact: @@ -5320,6 +5742,10 @@ def with_specification(pact: PactHandle, version: PactSpecification) -> None: version: The spec version to use. + + Raises: + RuntimeError: + If the Pact specification could not be set. """ success: bool = lib.pactffi_with_specification(pact._ref, version.value) if not success: @@ -5332,7 +5758,7 @@ def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification: Fetches the Pact specification version for the given Pact model. [Rust - `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_handle_get_pact_spec_version) + `pactffi_handle_get_pact_spec_version`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_handle_get_pact_spec_version) Args: handle: @@ -5344,43 +5770,119 @@ def handle_get_pact_spec_version(handle: PactHandle) -> PactSpecification: return PactSpecification(lib.pactffi_handle_get_pact_spec_version(handle._ref)) -def with_pact_metadata( - pact: PactHandle, - namespace: str, - name: str, - value: str, -) -> None: - """ - Sets the additional metadata on the Pact file. +def with_pact_metadata( + pact: PactHandle, + namespace: str, + name: str, + value: str, +) -> None: + """ + Sets the additional metadata on the Pact file. + + Common uses are to add the client library details such as the name and + version Returns false if the interaction or Pact can't be modified (i.e. the + mock server for it has already started) + + [Rust + `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_pact_metadata) + + Args: + pact: + Handle to a Pact model + + namespace: + The top level metadat key to set any key values on + + name: + The key to set + + value: + The value to set + + Raises: + RuntimeError: + If the metadata could not be set. + """ + success: bool = lib.pactffi_with_pact_metadata( + pact._ref, + namespace.encode("utf-8"), + name.encode("utf-8"), + value.encode("utf-8"), + ) + if not success: + msg = f"Failed to set Pact metadata for {pact} with {namespace}.{name}={value}" + raise RuntimeError(msg) + + +def with_metadata( + interaction: InteractionHandle, + key: str, + value: str, + part: InteractionPart, +) -> None: + r""" + Adds metadata to the interaction. + + Metadata is only relevant for message interactions to provide additional + information about the message, such as the queue name, message type, tags, + timestamps, etc. + + * `key` - metadata key + * `value` - metadata value, supports JSON structures with matchers and + generators. Passing a `NULL` point will remove the metadata key instead. + * `part` - the part of the interaction to add the metadata to (only + relevant for synchronous message interactions). + + Returns `true` if the metadata was added successfully, `false` otherwise. + + To include matching rules for the value, include the matching rule JSON + format with the value as a single JSON document. I.e. + + ```python with_metadata( + handle, "TagData", json.dumps({ + "value": {"ID": "sjhdjkshsdjh", "weight": 100.5}, + "pact:matcher:type": "type", + }), + ) + ``` - Common uses are to add the client library details such as the name and - version Returns false if the interaction or Pact can't be modified (i.e. the - mock server for it has already started) + See + [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) - [Rust - `pactffi_with_pact_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_pact_metadata) + # Note - Args: - pact: - Handle to a Pact model + For HTTP interactions, use [`with_header_v2`][pact.v3.ffi.with_header_v2] + instead. This function will not have any effect on HTTP interactions and + returns `false`. - namespace: - The top level metadat key to set any key values on + For synchronous message interactions, the `part` parameter is required to + specify whether the metadata should be added to the request or response + part. For responses which can have multiple messages, the metadata will be + set on all response messages. This also requires for responses to have been + defined in the interaction. - name: - The key to set + The [`with_body`][pact.v3.ffi.with_body] will also contribute to the + metadata of the message (both sync and async) by setting the key + `contentType` with the content type of the message. - value: - The value to set + # Safety + + The key and value parameters must be valid pointers to NULL terminated + strings, or `NULL` for the value parameter if the metadata key should be + removed. + + Raises: + RuntimeError: + If the metadata could not be set. """ - success: bool = lib.pactffi_with_pact_metadata( - pact._ref, - namespace.encode("utf-8"), - name.encode("utf-8"), + success: bool = lib.pactffi_with_metadata( + interaction._ref, + key.encode("utf-8"), value.encode("utf-8"), + part.value, ) if not success: - msg = f"Failed to set Pact metadata for {pact} with {namespace}.{name}={value}" + msg = f"Failed to set metadata for {interaction} with {key}={value}" raise RuntimeError(msg) @@ -5394,7 +5896,7 @@ def with_header_v2( r""" Configures a header for the Interaction. - [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_header_v2) + [Rust `pactffi_with_header_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_header_v2) To setup a header with multiple values, you can either call this function multiple times with a different index value: @@ -5432,7 +5934,7 @@ def with_header_v2( ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -5454,10 +5956,11 @@ def with_header_v2( This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). Raises: - RuntimeError: If there was an error setting the header. + RuntimeError: + If there was an error setting the header. """ success: bool = lib.pactffi_with_header_v2( interaction._ref, @@ -5485,7 +5988,7 @@ def set_header( and generators can not be configured with it. [Rust - `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_set_header) + `pactffi_set_header`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_header) If matching rules are required to be set, use `pactffi_with_header_v2`. @@ -5504,7 +6007,8 @@ def set_header( The header value. This is handled as-is, with no processing. Raises: - RuntimeError: If the header could not be set. + RuntimeError: + If the header could not be set. """ success: bool = lib.pactffi_set_header( interaction._ref, @@ -5522,7 +6026,7 @@ def response_status(interaction: InteractionHandle, status: int) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_response_status) + `pactffi_response_status`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_status) Args: interaction: @@ -5532,7 +6036,8 @@ def response_status(interaction: InteractionHandle, status: int) -> None: The response status. Defaults to 200. Raises: - RuntimeError: If the response status could not be set. + RuntimeError: + If the response status could not be set. """ success: bool = lib.pactffi_response_status(interaction._ref, status) if not success: @@ -5545,7 +6050,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: Configures the response for the Interaction. [Rust - `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_response_status_v2) + `pactffi_response_status_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_response_status_v2) To include matching rules for the status (only statusCode or integer really makes sense to use), include the matching rule JSON format with the value as @@ -5564,7 +6069,7 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: ) ``` - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) + See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) Args: interaction: @@ -5575,10 +6080,11 @@ def response_status_v2(interaction: InteractionHandle, status: str) -> None: This may be a simple string in which case it will be used as-is, or it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). + rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). Raises: - RuntimeError: If the response status could not be set. + RuntimeError: + If the response status could not be set. """ success: bool = lib.pactffi_response_status_v2( interaction._ref, status.encode("utf-8") @@ -5598,7 +6104,7 @@ def with_body( Adds the body for the interaction. [Rust - `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_body) + `pactffi_with_body`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_body) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -5619,10 +6125,11 @@ def with_body( body: The body contents. For JSON payloads, matching rules can be embedded - in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). + in the body. See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). Raises: - RuntimeError: If the body could not be specified. + RuntimeError: + If the body could not be specified. """ success: bool = lib.pactffi_with_body( interaction._ref, @@ -5645,7 +6152,7 @@ def with_binary_body( Adds the body for the interaction. [Rust - `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_binary_body) + `pactffi_with_binary_body`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_binary_body) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -5669,7 +6176,8 @@ def with_binary_body( The body contents. If `None`, the body will be set to null. Raises: - RuntimeError: If the body could not be modified. + RuntimeError: + If the body could not be modified. """ raise NotImplementedError @@ -5688,7 +6196,7 @@ def with_binary_file( already started) [Rust - `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_binary_file) + `pactffi_with_binary_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_binary_file) For HTTP and async message interactions, this will overwrite the body. With asynchronous messages, the part parameter will be ignored. With synchronous @@ -5709,6 +6217,10 @@ def with_binary_file( body: The body contents. If `None`, the body will be set to null. + + Raises: + RuntimeError: + If the body could not be set. """ if len(gc.get_referrers(body)) == 0: warnings.warn( @@ -5738,7 +6250,7 @@ def with_matching_rules( Add matching rules to the interaction. [Rust - `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_matching_rules) + `pactffi_with_matching_rules`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_matching_rules) This function can be called multiple times, in which case the matching rules will be merged. @@ -5754,7 +6266,8 @@ def with_matching_rules( JSON string of the matching rules to add to the interaction. Raises: - RuntimeError: If the rules could not be added. + RuntimeError: + If the rules could not be added. """ success: bool = lib.pactffi_with_matching_rules( interaction._ref, @@ -5766,6 +6279,47 @@ def with_matching_rules( raise RuntimeError(msg) +def with_generators( + interaction: InteractionHandle, + part: InteractionPart, + generators: str, +) -> None: + """ + Add generators to the interaction. + + [Rust + `pactffi_with_generators`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_generators) + + This function can be called multiple times, in which case the generators + will be combined (provide they don't clash). + + For synchronous messages which allow multiple responses, the generators will + be added to all the responses. + + Args: + interaction: + Handle to the Interaction. + + part: + Request or response part (if applicable). + + generators: + JSON string of the generators to add to the interaction. + + Raises: + RuntimeError: + If the generators could not be added. + """ + success: bool = lib.pactffi_with_generators( + interaction._ref, + part.value, + generators.encode("utf-8"), + ) + if not success: + msg = f"Unable to set generators for {interaction}." + raise RuntimeError(msg) + + def with_multipart_file_v2( # noqa: PLR0913 interaction: InteractionHandle, part: InteractionPart, @@ -5782,7 +6336,7 @@ def with_multipart_file_v2( # noqa: PLR0913 already started) or an error occurs. [Rust - `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_multipart_file_v2) + `pactffi_with_multipart_file_v2`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_multipart_file_v2) This function can be called multiple times. In that case, each subsequent call will be appended to the existing multipart body as a new part. @@ -5836,7 +6390,7 @@ def with_multipart_file( already started) or an error occurs. [Rust - `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_multipart_file) + `pactffi_with_multipart_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_with_multipart_file) * `interaction` - Interaction handle to set the body for. * `part` - Request or response part. @@ -5873,7 +6427,7 @@ def set_key(interaction: InteractionHandle, key: str | None) -> None: Sets the key attribute for the interaction. [Rust - `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_set_key) + `pactffi_set_key`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_key) Args: interaction: @@ -5882,6 +6436,10 @@ def set_key(interaction: InteractionHandle, key: str | None) -> None: key: Key value. This must be a valid UTF-8 null-terminated string, or `None` to clear the key. + + Raises: + RuntimeError: + If the key could not be set. """ success: bool = lib.pactffi_set_key( interaction._ref, @@ -5897,7 +6455,7 @@ def set_pending(interaction: InteractionHandle, *, pending: bool) -> None: Mark the interaction as pending. [Rust - `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_set_pending) + `pactffi_set_pending`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_pending) Args: interaction: @@ -5905,6 +6463,10 @@ def set_pending(interaction: InteractionHandle, *, pending: bool) -> None: pending: Boolean value to toggle the pending state of the interaction. + + Raises: + RuntimeError: + If the pending status could not be updated. """ success: bool = lib.pactffi_set_pending(interaction._ref, pending) if not success: @@ -5917,7 +6479,7 @@ def set_comment(interaction: InteractionHandle, key: str, value: str | None) -> Add a comment to the interaction. [Rust - `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_set_comment) + `pactffi_set_comment`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_set_comment) Args: interaction: @@ -5933,7 +6495,8 @@ def set_comment(interaction: InteractionHandle, key: str, value: str | None) -> null. Raises: - RuntimeError: If the comments could not be updated. + RuntimeError: + If the comments could not be updated. """ success: bool = lib.pactffi_set_comment( interaction._ref, @@ -5950,7 +6513,7 @@ def add_text_comment(interaction: InteractionHandle, comment: str) -> None: Add a text comment to the interaction. [Rust - `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_add_text_comment) + `pactffi_add_text_comment`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_add_text_comment) Args: interaction: @@ -5958,6 +6521,10 @@ def add_text_comment(interaction: InteractionHandle, comment: str) -> None: comment: Comment value. This is a regular string value. + + Raises: + RuntimeError: + If the comment could not be added. """ success: bool = lib.pactffi_add_text_comment( interaction._ref, @@ -5968,12 +6535,15 @@ def add_text_comment(interaction: InteractionHandle, comment: str) -> None: raise RuntimeError(msg) -def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator: +def pact_handle_get_async_message_iter(pact: PactHandle) -> PactAsyncMessageIterator: r""" - Get an iterator over all the messages of the Pact. + Get an iterator over all the asynchronous messages of the Pact. + + The returned iterator needs to be freed with + `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_message_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_get_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -5986,7 +6556,9 @@ def pact_handle_get_message_iter(pact: PactHandle) -> PactMessageIterator: This function may fail if any of the Rust strings contain embedded null ('\0') bytes. """ - return PactMessageIterator(lib.pactffi_pact_handle_get_message_iter(pact._ref)) + return PactAsyncMessageIterator( + lib.pactffi_pact_handle_get_async_message_iter(pact._ref), + ) def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterator: @@ -5997,7 +6569,7 @@ def pact_handle_get_sync_message_iter(pact: PactHandle) -> PactSyncMessageIterat `pactffi_pact_sync_message_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) + `pactffi_pact_handle_get_sync_message_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_get_sync_message_iter) # Safety @@ -6023,7 +6595,7 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: `pactffi_pact_sync_http_iter_delete`. [Rust - `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) + `pactffi_pact_handle_get_sync_http_iter`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_get_sync_http_iter) # Safety @@ -6039,249 +6611,6 @@ def pact_handle_get_sync_http_iter(pact: PactHandle) -> PactSyncHttpIterator: return PactSyncHttpIterator(lib.pactffi_pact_handle_get_sync_http_iter(pact._ref)) -def new_message_pact(consumer_name: str, provider_name: str) -> MessagePactHandle: - """ - Creates a new Pact Message model and returns a handle to it. - - [Rust - `pactffi_new_message_pact`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_message_pact) - - * `consumer_name` - The name of the consumer for the pact. - * `provider_name` - The name of the provider for the pact. - - Returns a new `MessagePactHandle`. The handle will need to be freed with the - `pactffi_free_message_pact_handle` function to release its resources. - """ - raise NotImplementedError - - -def new_message(pact: MessagePactHandle, description: str) -> MessageHandle: - """ - Creates a new Message and returns a handle to it. - - [Rust - `pactffi_new_message`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_new_message) - - * `description` - The message description. It needs to be unique for each - Message. - - Returns a new `MessageHandle`. - """ - raise NotImplementedError - - -def message_expects_to_receive(message: MessageHandle, description: str) -> None: - """ - Sets the description for the Message. - - [Rust - `pactffi_message_expects_to_receive`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_expects_to_receive) - - * `description` - The message description. It needs to be unique for each - message. - """ - raise NotImplementedError - - -def message_given(message: MessageHandle, description: str) -> None: - """ - Adds a provider state to the Interaction. - - [Rust - `pactffi_message_given`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_given) - - * `description` - The provider state description. It needs to be unique for - each message - """ - raise NotImplementedError - - -def message_given_with_param( - message: MessageHandle, - description: str, - name: str, - value: str, -) -> None: - """ - Adds a provider state to the Message with a parameter key and value. - - [Rust - `pactffi_message_given_with_param`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_given_with_param) - - * `description` - The provider state description. It needs to be unique. - * `name` - Parameter name. - * `value` - Parameter value. - """ - raise NotImplementedError - - -def message_with_contents( - message_handle: MessageHandle, - content_type: str, - body: List[int], - size: int, -) -> None: - """ - Adds the contents of the Message. - - [Rust - `pactffi_message_with_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_with_contents) - - Accepts JSON, binary and other payload types. Binary data will be base64 - encoded when serialised. - - Note: For text bodies (plain text, JSON or XML), you can pass in a C string - (NULL terminated) and the size of the body is not required (it will be - ignored). For binary bodies, you need to specify the number of bytes in the - body. - - * `content_type` - The content type of the body. Defaults to `text/plain`, - supports JSON structures with matchers and binary data. - * `body` - The body contents as bytes. For text payloads (JSON, XML, etc.), - a C string can be used and matching rules can be embedded in the body. - * `content_type` - Expected content type (e.g. application/json, - application/octet-stream) - * `size` - number of bytes in the message body to read. This is not required - for text bodies (JSON, XML, etc.). - """ - raise NotImplementedError - - -def message_with_metadata(message_handle: MessageHandle, key: str, value: str) -> None: - """ - Adds expected metadata to the Message. - - [Rust - `pactffi_message_with_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_with_metadata) - - * `key` - metadata key - * `value` - metadata value. - """ - raise NotImplementedError - - -def message_with_metadata_v2( - message_handle: MessageHandle, - key: str, - value: str, -) -> None: - """ - Adds expected metadata to the Message. - - [Rust - `pactffi_message_with_metadata_v2`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_with_metadata_v2) - - Args: - message_handle: - Handle to the Message. - - key: - Metadata key. - - value: - Metadata value. - - This may be a simple string in which case it will be used as-is, or - it may be a [JSON matching - rule](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). - - To include matching rules for the metadata, include the matching rule JSON - format with the value as a single JSON document. I.e. - - ```python - message_with_metadata_v2( - handle, - "contentType", - json.dumps({ - "pact:matcher:type": "regex", - "regex": "text/.*", - }), - ) - ``` - - See [IntegrationJson.md](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). - """ - raise NotImplementedError - - -def message_reify(message_handle: MessageHandle) -> OwnedString: - """ - Reifies the given message. - - [Rust - `pactffi_message_reify`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_message_reify) - - Reification is the process of stripping away any matchers, and returning the - original contents. - - # Safety - - The returned string needs to be deallocated with the `free_string` function. - This function must only ever be called from a foreign language. Calling it - from a Rust function that has a Tokio runtime in its call stack can result - in a deadlock. - """ - raise NotImplementedError - - -def write_message_pact_file( - pact: MessagePactHandle, - directory: str, - *, - overwrite: bool, -) -> int: - """ - External interface to write out the message pact file. - - This function should be called if all the consumer tests have passed. The - directory to write the file to is passed as the second parameter. If a NULL - pointer is passed, the current working directory is used. - - [Rust - `pactffi_write_message_pact_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_write_message_pact_file) - - If overwrite is true, the file will be overwritten with the contents of the - current pact. Otherwise, it will be merged with any existing pact file. - - Returns 0 if the pact file was successfully written. Returns a positive code - if the file can not be written, or there is no mock server running on that - port or the function panics. - - # Errors - - Errors are returned as positive values. - - | Error | Description | - |-------|-------------| - | 1 | The pact file was not able to be written | - | 2 | The message pact for the given handle was not found | - """ - raise NotImplementedError - - -def with_message_pact_metadata( - pact: MessagePactHandle, - namespace_: str, - name: str, - value: str, -) -> None: - """ - Sets the additional metadata on the Pact file. - - Common uses are to add the client library details such as the name and - version - - [Rust - `pactffi_with_message_pact_metadata`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_with_message_pact_metadata) - - * `pact` - Handle to a Pact model - * `namespace` - the top level metadat key to set any key values on - * `name` - the key to set - * `value` - the value to set - """ - raise NotImplementedError - - def pact_handle_write_file( pact: PactHandle, directory: Path | str | None, @@ -6292,7 +6621,7 @@ def pact_handle_write_file( External interface to write out the pact file. [Rust - `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_pact_handle_write_file) + `pactffi_pact_handle_write_file`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_pact_handle_write_file) This function should be called if all the consumer tests have passed. @@ -6308,6 +6637,10 @@ def pact_handle_write_file( If `True`, the file will be overwritten with the contents of the current pact. Otherwise, it will be merged with any existing pact file. + + Raises: + RuntimeError: + If there was an error writing the pact file. """ ret: int = lib.pactffi_pact_handle_write_file( pact._ref, @@ -6332,10 +6665,11 @@ def free_pact_handle(pact: PactHandle) -> None: Delete a Pact handle and free the resources used by it. [Rust - `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_free_pact_handle) + `pactffi_free_pact_handle`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_free_pact_handle) Raises: - RuntimeError: If the handle could not be freed. + RuntimeError: + If the handle could not be freed. """ ret: int = lib.pactffi_free_pact_handle(pact._ref) if ret == 0: @@ -6347,29 +6681,11 @@ def free_pact_handle(pact: PactHandle) -> None: raise RuntimeError(msg) -def free_message_pact_handle(pact: MessagePactHandle) -> int: - """ - Delete a Pact handle and free the resources used by it. - - [Rust - `pactffi_free_message_pact_handle`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_free_message_pact_handle) - - # Error Handling - - On failure, this function will return a positive integer value. - - * `1` - The handle is not valid or does not refer to a valid Pact. Could be - that it was previously deleted. - - """ - raise NotImplementedError - - def verify(args: str) -> int: """ External interface to verifier a provider. - [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verify) + [Rust `pactffi_verify`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verify) * `args` - the same as the CLI interface, except newline delimited @@ -6396,7 +6712,7 @@ def verifier_new_for_application() -> VerifierHandle: Get a Handle to a newly created verifier. [Rust - `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_new_for_application) + `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_new_for_application) """ from pact import __version__ @@ -6411,7 +6727,7 @@ def verifier_shutdown(handle: VerifierHandle) -> None: """ Shutdown the verifier and release all resources. - [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_shutdown) + [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_shutdown) """ lib.pactffi_verifier_shutdown(handle._ref) @@ -6428,7 +6744,7 @@ def verifier_set_provider_info( # noqa: PLR0913 Set the provider details for the Pact verifier. [Rust - `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_provider_info) + `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_provider_info) Args: handle: @@ -6474,7 +6790,7 @@ def verifier_add_provider_transport( Adds a new transport for the given provider. [Rust - `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_add_provider_transport) + `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_provider_transport) Args: handle: @@ -6515,7 +6831,7 @@ def verifier_set_filter_info( Set the filters for the Pact verifier. [Rust - `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_filter_info) + `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_filter_info) Set filters to narrow down the interactions to verify. @@ -6551,7 +6867,7 @@ def verifier_set_provider_state( Set the provider state URL for the Pact verifier. [Rust - `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_provider_state) + `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_provider_state) Args: handle: @@ -6586,7 +6902,7 @@ def verifier_set_verification_options( Set the options used by the verifier when calling the provider. [Rust - `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_verification_options) + `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_verification_options) Args: handle: @@ -6597,6 +6913,10 @@ def verifier_set_verification_options( request_timeout: The timeout for the request in milliseconds. + + Raises: + RuntimeError: + If the options could not be set. """ retval: int = lib.pactffi_verifier_set_verification_options( handle._ref, @@ -6617,7 +6937,7 @@ def verifier_set_coloured_output( Enables or disables coloured output using ANSI escape codes. [Rust - `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_coloured_output) + `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_coloured_output) By default, coloured output is enabled. @@ -6627,6 +6947,10 @@ def verifier_set_coloured_output( enabled: A boolean value to enable or disable coloured output. + + Raises: + RuntimeError: + If the coloured output could not be set. """ retval: int = lib.pactffi_verifier_set_coloured_output( handle._ref, @@ -6642,7 +6966,7 @@ def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> Enables or disables if no pacts are found to verify results in an error. [Rust - `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) + `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) Args: handle: @@ -6650,6 +6974,10 @@ def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> enabled: If `True`, an error will be raised when no pacts are found to verify. + + Raises: + RuntimeError: + If the no pacts is error setting could not be set. """ retval: int = lib.pactffi_verifier_set_no_pacts_is_error( handle._ref, @@ -6671,7 +6999,7 @@ def verifier_set_publish_options( Set the options used when publishing verification results to the Broker. [Rust - `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_publish_options) + `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_publish_options) Args: handle: @@ -6688,6 +7016,10 @@ def verifier_set_publish_options( provider_branch: Name of the branch used for verification. + + Raises: + RuntimeError: + If the publish options could not be set. """ retval: int = lib.pactffi_verifier_set_publish_options( handle._ref, @@ -6710,7 +7042,7 @@ def verifier_set_consumer_filters( Set the consumer filters for the Pact verifier. [Rust - `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_set_consumer_filters) + `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_set_consumer_filters) """ lib.pactffi_verifier_set_consumer_filters( handle._ref, @@ -6728,7 +7060,7 @@ def verifier_add_custom_header( Adds a custom header to be added to the requests made to the provider. [Rust - `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_add_custom_header) + `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_custom_header) """ lib.pactffi_verifier_add_custom_header( handle._ref, @@ -6742,7 +7074,7 @@ def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: Adds a Pact file as a source to verify. [Rust - `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_add_file_source) + `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_file_source) """ lib.pactffi_verifier_add_file_source(handle._ref, file.encode("utf-8")) @@ -6754,7 +7086,7 @@ def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> Non All pacts from the directory that match the provider name will be verified. [Rust - `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_add_directory_source) + `pactffi_verifier_add_directory_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_add_directory_source) # Safety @@ -6776,7 +7108,7 @@ def verifier_url_source( Adds a URL as a source to verify. [Rust - `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_url_source) + `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_url_source) Args: handle: @@ -6816,7 +7148,7 @@ def verifier_broker_source( Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_broker_source) + `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_broker_source) This will fetch all the pact files from the broker that match the provider name. @@ -6864,7 +7196,7 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 Adds a Pact broker as a source to verify. [Rust - `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) + `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) This will fetch all the pact files from the broker that match the provider name and the consumer version selectors (See [Consumer Version @@ -6912,9 +7244,11 @@ def verifier_broker_source_with_selectors( # noqa: PLR0913 password.encode("utf-8") if password else ffi.NULL, token.encode("utf-8") if token else ffi.NULL, enable_pending, - include_wip_pacts_since.isoformat().encode("utf-8") - if include_wip_pacts_since - else ffi.NULL, + ( + include_wip_pacts_since.isoformat().encode("utf-8") + if include_wip_pacts_since + else ffi.NULL + ), [ffi.new("char[]", t.encode("utf-8")) for t in provider_tags], len(provider_tags), provider_branch.encode("utf-8") if provider_branch else ffi.NULL, @@ -6929,7 +7263,11 @@ def verifier_execute(handle: VerifierHandle) -> None: """ Runs the verification. - (https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_execute) + (https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_execute) + + Raises: + RuntimeError: + If the verifier could not be executed. """ success: int = lib.pactffi_verifier_execute(handle._ref) if success != 0: @@ -6945,7 +7283,7 @@ def verifier_cli_args() -> str: string. [Rust - `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_cli_args) + `pactffi_verifier_cli_args`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_cli_args) The purpose is to then be able to use in other languages which wrap the FFI library, to implement the same CLI functionality automatically without @@ -7001,11 +7339,15 @@ def verifier_logs(handle: VerifierHandle) -> OwnedString: Extracts the logs for the verification run. [Rust - `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_logs) + `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_logs) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` function call to avoid leaking memory. + + Raises: + RuntimeError: + If the logs could not be extracted. """ ptr = lib.pactffi_verifier_logs(handle._ref) if ptr == ffi.NULL: @@ -7019,11 +7361,15 @@ def verifier_logs_for_provider(provider_name: str) -> OwnedString: Extracts the logs for the verification run for the provider name. [Rust - `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_logs_for_provider) + `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_logs_for_provider) This needs the memory buffer log sink to be setup before the verification is executed. The returned string will need to be freed with the `free_string` function call to avoid leaking memory. + + Raises: + RuntimeError: + If the logs could not be extracted. """ ptr = lib.pactffi_verifier_logs_for_provider(provider_name.encode("utf-8")) if ptr == ffi.NULL: @@ -7037,7 +7383,7 @@ def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: Extracts the standard output for the verification run. [Rust - `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_output) + `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_output) Args: handle: @@ -7047,6 +7393,10 @@ def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: This parameter controls ANSI escape codes. Setting it to a non-zero value will cause the ANSI control codes to be stripped from the output. + + Raises: + RuntimeError: + If the output could not be extracted. """ ptr = lib.pactffi_verifier_output(handle._ref, strip_ansi) if ptr == ffi.NULL: @@ -7060,7 +7410,11 @@ def verifier_json(handle: VerifierHandle) -> OwnedString: Extracts the verification result as a JSON document. [Rust - `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_verifier_json) + `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_verifier_json) + + Raises: + RuntimeError: + If the JSON could not be extracted. """ ptr = lib.pactffi_verifier_json(handle._ref) if ptr == ffi.NULL: @@ -7084,7 +7438,7 @@ def using_plugin( otherwise you will have plugin processes left running. [Rust - `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_using_plugin) + `pactffi_using_plugin`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_using_plugin) Args: pact: @@ -7096,6 +7450,10 @@ def using_plugin( plugin_version: Version of the plugin to use. If `None`, the latest version will be used. + + Raises: + RuntimeError: + If the plugin could not be loaded. """ ret: int = lib.pactffi_using_plugin( pact._ref, @@ -7123,7 +7481,7 @@ def cleanup_plugins(pact: PactHandle) -> None: zero). [Rust - `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_cleanup_plugins) + `pactffi_cleanup_plugins`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_cleanup_plugins) """ lib.pactffi_cleanup_plugins(pact._ref) @@ -7142,7 +7500,7 @@ def interaction_contents( format of the JSON contents. [Rust - `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_interaction_contents) + `pactffi_interaction_contents`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_interaction_contents) Args: interaction: @@ -7157,6 +7515,10 @@ def interaction_contents( contents: JSON contents that gets passed to the plugin. + + Raises: + RuntimeError: + If the interaction could not be configured """ ret: int = lib.pactffi_interaction_contents( interaction._ref, @@ -7198,7 +7560,7 @@ def matches_string_value( function once it is no longer required. [Rust - `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_string_value) + `pactffi_matches_string_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_string_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string @@ -7229,7 +7591,7 @@ def matches_u64_value( function once it is no longer required. [Rust - `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_u64_value) + `pactffi_matches_u64_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_u64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7259,7 +7621,7 @@ def matches_i64_value( function once it is no longer required. [Rust - `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_i64_value) + `pactffi_matches_i64_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_i64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7289,7 +7651,7 @@ def matches_f64_value( function once it is no longer required. [Rust - `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_f64_value) + `pactffi_matches_f64_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_f64_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7319,7 +7681,7 @@ def matches_bool_value( function once it is no longer required. [Rust - `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_bool_value) + `pactffi_matches_bool_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_bool_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get, 0 == false and 1 == true @@ -7351,7 +7713,7 @@ def matches_binary_value( # noqa: PLR0913 function once it is no longer required. [Rust - `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_binary_value) + `pactffi_matches_binary_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_binary_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get @@ -7386,7 +7748,7 @@ def matches_json_value( function once it is no longer required. [Rust - `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.19/pact_ffi/?search=pactffi_matches_json_value) + `pactffi_matches_json_value`](https://docs.rs/pact_ffi/0.4.21/pact_ffi/?search=pactffi_matches_json_value) * matching_rule - pointer to a matching rule * expected_value - value we expect to get as a NULL terminated string diff --git a/src/pact/v3/interaction/_async_message_interaction.py b/src/pact/v3/interaction/_async_message_interaction.py index 011853af3..15621650a 100644 --- a/src/pact/v3/interaction/_async_message_interaction.py +++ b/src/pact/v3/interaction/_async_message_interaction.py @@ -33,7 +33,7 @@ def __init__(self, pact_handle: pact.v3.ffi.PactHandle, description: str) -> Non Args: pact_handle: - Handle for the Pact. + The Pact instance this interaction belongs to. description: Description of the interaction. This must be unique within the @@ -54,4 +54,14 @@ def _handle(self) -> pact.v3.ffi.InteractionHandle: @property def _interaction_part(self) -> pact.v3.ffi.InteractionPart: + """ + Interaction part. + + Where interactions have multiple parts, this property keeps track + of which part is currently being set. + + As this is an asynchronous message interaction, this will always + return a [`REQUEST`][pact.v3.ffi.InteractionPart.REQUEST], as there the + consumer of the message does not send any responses. + """ return pact.v3.ffi.InteractionPart.REQUEST diff --git a/src/pact/v3/interaction/_base.py b/src/pact/v3/interaction/_base.py index 1b375d29a..9696f37a3 100644 --- a/src/pact/v3/interaction/_base.py +++ b/src/pact/v3/interaction/_base.py @@ -37,6 +37,19 @@ class Interaction(abc.ABC): - [`HttpInteraction`][pact.v3.interaction.HttpInteraction] - [`AsyncMessageInteraction`][pact.v3.interaction.AsyncMessageInteraction] - [`SyncMessageInteraction`][pact.v3.interaction.SyncMessageInteraction] + + # Interaction Part + + For HTTP and synchronous message interactions, the interaction is split into + two parts: the request and the response. The interaction part is used to + specify which part of the interaction is being set. This is specified using + the `part` argument of various methods (which defaults to an intelligent + choice based on the order of the methods called). + + The asynchronous message interaction does not have parts, as the interaction + contains a single message from the provider (a.ka. the producer of the + message) to the consumer. An attempt to set a response part will raise an + error. """ def __init__(self, description: str) -> None: @@ -295,6 +308,73 @@ def with_binary_body( ) return self + def with_metadata( + self, + __metadata: dict[str, str] | None = None, + __part: Literal["Request", "Response"] | None = None, + /, + **kwargs: str, + ) -> Self: + """ + Set metadata for the interaction. + + This function may either be called with a single dictionary of metadata, + or with keyword arguments that are the key-value pairs of the metadata + (or a combination therefore): + + ```python + interaction.with_metadata({"key": "value", "key two": "value two"}) + interaction.with_metadata(foo="bar", baz="qux") + ``` + + The value of `None` will remove the metadata key from the interaction. + This is distinct from using an empty string or a string containing the + JSON `null` value, which will set the metadata key to an empty string or + the JSON `null` value, respectively. + + !!! note + + There are two special keys which cannot be used as keyword + arguments: `__metadata` and `__part`. Should there ever be a need + to set metadata with one of these keys, they must be passed through + as a dictionary: + + ```python + interaction.with_metadata({"__metadata": "value", "__part": 1}) + ``` + + Args: + ___metadata: + Dictionary of metadata keys and associated values. + + __part: + Whether the metadata should be added to the request or the + response. If `None`, then the function intelligently determines + whether the body should be added to the request or the response. + + **kwargs: + Additional metadata key-value pairs. + + Returns: + The current instance of the interaction. + """ + part = self._parse_interaction_part(__part) + for k, v in (__metadata or {}).items(): + pact.v3.ffi.with_metadata( + self._handle, + k, + v, + part, + ) + for k, v in kwargs.items(): + pact.v3.ffi.with_metadata( + self._handle, + k, + v, + part, + ) + return self + def with_multipart_file( # noqa: PLR0913 self, part_name: str, @@ -471,3 +551,36 @@ def with_matching_rules( rules, ) return self + + def with_generators( + self, + generators: dict[str, Any] | str, + part: Literal["Request", "Response"] | None = None, + ) -> Self: + """ + Add generators to the interaction. + + Generators are used to adjust how parts of the request or response are + generated when the Pact is being tested. This can be useful for fields + that vary each time the request is made, such as a timestamp. + + Args: + generators: + Generators to add to the interaction. This must be encodable using + [`json.dumps(...)`][json.dumps], or a string. + + part: + Whether the generators should be added to the request or the + response. If `None`, then the function intelligently determines + whether the generators should be added to the request or the + response. + """ + if isinstance(generators, dict): + generators = json.dumps(generators) + + pact.v3.ffi.with_generators( + self._handle, + self._parse_interaction_part(part), + generators, + ) + return self diff --git a/src/pact/v3/interaction/_http_interaction.py b/src/pact/v3/interaction/_http_interaction.py index 335d6293c..1f00bf194 100644 --- a/src/pact/v3/interaction/_http_interaction.py +++ b/src/pact/v3/interaction/_http_interaction.py @@ -149,7 +149,7 @@ def with_header( # JSON Matching Pact's matching rules are defined in the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md) + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md) and support a wide range of matching rules. These can be specified using a JSON object as a strong using `json.dumps(...)`. For example, the above rule whereby the `X-Foo` header has multiple values can be @@ -391,7 +391,7 @@ def with_query_parameter(self, name: str, value: str) -> Self: ``` For more information on the format of the JSON object, see the [upstream - documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.19/rust/pact_ffi/IntegrationJson.md). + documentation](https://github.com/pact-foundation/pact-reference/blob/libpact_ffi-v0.4.21/rust/pact_ffi/IntegrationJson.md). Args: name: diff --git a/src/pact/v3/pact.py b/src/pact/v3/pact.py index 231c01dd7..1c0dd2fd4 100644 --- a/src/pact/v3/pact.py +++ b/src/pact/v3/pact.py @@ -64,8 +64,20 @@ import json import logging +import warnings +from abc import ABC from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, Set, overload +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generator, + List, + Literal, + Set, + overload, +) from yarl import URL @@ -87,6 +99,120 @@ logger = logging.getLogger(__name__) +class PactError(Exception, ABC): + """ + Base class for exceptions raised by the Pact module. + """ + + +class InteractionVerificationError(PactError): + """ + Exception raised due during the verification of an interaction. + + This error is raised when an error occurs during the manual verification of an + interaction. This is typically raised when the consumer fails to handle the + interaction correctly thereby generating its own exception. The cause of the + error is stored in the `error` attribute. + """ + + def __init__(self, description: str, error: Exception) -> None: + """ + Initialise a new InteractionVerificationError. + + Args: + description: + Description of the interaction that failed verification. + + error: Error that occurred during the verification of the + interaction. + """ + super().__init__(f"Error verifying interaction '{description}': {error}") + self._description = description + self._error = error + + @property + def description(self) -> str: + """ + Description of the interaction that failed verification. + """ + return self._description + + @property + def error(self) -> Exception: + """ + Error that occurred during the verification of the interaction. + """ + return self._error + + +class PactVerificationError(PactError): + """ + Exception raised due to errors in the verification of a Pact. + + This is raised when performing manual verification of the Pact through the + [`verify`][pact.v3.Pact.verify] method: + + ```python + pact = Pact("consumer", "provider") + # Define interactions... + try: + pact.verify(handler, kind="Async") + except PactVerificationError as e: + print(e.errors) + ``` + + All of the errors that occurred during the verification of all of the + interactions are stored in the `errors` attribute. + + This is different from the [`MismatchesError`][pact.v3.MismatchesError] + which is raised when there are mismatches detected by the mock server. + """ + + def __init__(self, errors: list[InteractionVerificationError]) -> None: + """ + Initialise a new PactVerificationError. + + Args: + errors: + Errors that occurred during the verification of the Pact. + """ + super().__init__(f"Error verifying Pact (count: {len(errors)})") + self._errors = errors + + @property + def errors(self) -> list[InteractionVerificationError]: + """ + Errors that occurred during the verification of the Pact. + """ + return self._errors + + +class MismatchesError(PactError): + """ + Exception raised when there are mismatches between the Pact and the server. + """ + + def __init__(self, mismatches: list[dict[str, Any]]) -> None: + """ + Initialise a new MismatchesError. + + Args: + mismatches: + Mismatches between the Pact and the server. + """ + super().__init__(f"Mismatched interaction (count: {len(mismatches)})") + self._mismatches = mismatches + + # TODO: Replace the list of dicts with a more structured object. + # https://github.com/pact-foundation/pact-python/issues/644 + @property + def mismatches(self) -> list[dict[str, Any]]: + """ + Mismatches between the Pact and the server. + """ + return self._mismatches + + class Pact: """ A Pact between a consumer and a provider. @@ -346,52 +472,31 @@ def serve( # noqa: PLR0913 verbose=verbose, ) - def messages(self) -> pact.v3.ffi.PactMessageIterator: - """ - Iterate over the messages in the Pact. - - This function returns an iterator over the messages in the Pact. This - is useful for validating the Pact against the provider. - - ```python - pact = Pact("consumer", "provider") - with pact.serve() as srv: - for message in pact.messages(): - # Validate the message against the provider. - ... - ``` - - Note that the Pact must be written to a file before the messages can be - iterated over. This is because the messages are not stored in memory, - but rather are streamed directly from the file. - """ - return pact.v3.ffi.pact_handle_get_message_iter(self._handle) - @overload def interactions( self, kind: Literal["HTTP"], - ) -> pact.v3.ffi.PactSyncHttpIterator: ... + ) -> Generator[pact.v3.ffi.SynchronousHttp, None, None]: ... @overload def interactions( self, kind: Literal["Sync"], - ) -> pact.v3.ffi.PactSyncMessageIterator: ... + ) -> Generator[pact.v3.ffi.SynchronousMessage, None, None]: ... @overload def interactions( self, kind: Literal["Async"], - ) -> pact.v3.ffi.PactMessageIterator: ... + ) -> Generator[pact.v3.ffi.AsynchronousMessage, None, None]: ... def interactions( self, - kind: str = "HTTP", + kind: Literal["HTTP", "Sync", "Async"] = "HTTP", ) -> ( - pact.v3.ffi.PactSyncHttpIterator - | pact.v3.ffi.PactSyncMessageIterator - | pact.v3.ffi.PactMessageIterator + Generator[pact.v3.ffi.SynchronousHttp, None, None] + | Generator[pact.v3.ffi.SynchronousMessage, None, None] + | Generator[pact.v3.ffi.AsynchronousMessage, None, None] ): """ Return an iterator over the Pact's interactions. @@ -402,13 +507,104 @@ def interactions( # TODO: Add an iterator for `All` interactions. # https://github.com/pact-foundation/pact-python/issues/451 if kind == "HTTP": - return pact.v3.ffi.pact_handle_get_sync_http_iter(self._handle) - if kind == "Sync": - return pact.v3.ffi.pact_handle_get_sync_message_iter(self._handle) - if kind == "Async": - return pact.v3.ffi.pact_handle_get_message_iter(self._handle) - msg = f"Unknown interaction type: {kind}" - raise ValueError(msg) + yield from pact.v3.ffi.pact_handle_get_sync_http_iter(self._handle) + elif kind == "Sync": + yield from pact.v3.ffi.pact_handle_get_sync_message_iter(self._handle) + elif kind == "Async": + yield from pact.v3.ffi.pact_handle_get_async_message_iter(self._handle) + else: + msg = f"Unknown interaction type: {kind}" + raise ValueError(msg) + return # Ensures that the parent object outlives the generator + + @overload + def verify( + self, + handler: Callable[[str | bytes | None, Dict[str, str]], None], + kind: Literal["Async", "Sync"], + *, + raises: Literal[True] = True, + ) -> None: ... + @overload + def verify( + self, + handler: Callable[[str | bytes | None, Dict[str, str]], None], + kind: Literal["Async", "Sync"], + *, + raises: Literal[False], + ) -> List[InteractionVerificationError]: ... + + def verify( + self, + handler: Callable[[str | bytes | None, Dict[str, str]], None], + kind: Literal["Async", "Sync"], + *, + raises: bool = True, + ) -> List[InteractionVerificationError] | None: + """ + Verify message interactions. + + This function is used to ensure that the consumer is able to handle the + messages that are defined in the Pact. The `handler` function is called + for each message in the Pact. + + The end-user is responsible for defining the `handler` function and + verifying that the messages are handled correctly. For example, if the + handler is meant to call an API, then the API call should be mocked out + and once the verification is complete, the mock should be verified. Any + exceptions raised by the handler will be caught and reported as + mismatches. + + Args: + handler: + The function that will be called for each message in the Pact. + + The first argument to the function is the message body, either as + a string or byte array. + + The second argument is the metadata for the message. If there + is no metadata, then this will be an empty dictionary. + + kind: + The type of message interaction. This must be one of `Async` + or `Sync`. + + raises: + Whether or not to raise an exception if the handler fails to + process a message. If set to `False`, then the function will + return a list of errors. + """ + errors: List[InteractionVerificationError] = [] + for message in self.interactions(kind): + request: pact.v3.ffi.MessageContents | None = None + if isinstance(message, pact.v3.ffi.SynchronousMessage): + request = message.request_contents + elif isinstance(message, pact.v3.ffi.AsynchronousMessage): + request = message.contents + else: + msg = f"Unknown message type: {type(message).__name__}" + raise TypeError(msg) + + if request is None: + warnings.warn( + f"Message '{message.description}' has no contents", + stacklevel=2, + ) + continue + + body = request.contents + metadata = {pair.key: pair.value for pair in request.metadata} + + try: + handler(body, metadata) + except Exception as e: # noqa: BLE001 + errors.append(InteractionVerificationError(message.description, e)) + + if raises: + if errors: + raise PactVerificationError(errors) + return None + return errors def write_file( self, @@ -443,32 +639,6 @@ def write_file( ) -class MismatchesError(Exception): - """ - Exception raised when there are mismatches between the Pact and the server. - """ - - def __init__(self, mismatches: list[dict[str, Any]]) -> None: - """ - Initialise a new MismatchesError. - - Args: - mismatches: - Mismatches between the Pact and the server. - """ - super().__init__(f"Mismatched interaction (count: {len(mismatches)})") - self._mismatches = mismatches - - # TODO: Replace the list of dicts with a more structured object. - # https://github.com/pact-foundation/pact-python/issues/644 - @property - def mismatches(self) -> list[dict[str, Any]]: - """ - Mismatches between the Pact and the server. - """ - return self._mismatches - - class PactServer: """ Pact Server. diff --git a/tests/v3/compatibility_suite/test_v3_consumer.py b/tests/v3/compatibility_suite/test_v3_consumer.py index b7014d6f7..f2f218a3f 100644 --- a/tests/v3/compatibility_suite/test_v3_consumer.py +++ b/tests/v3/compatibility_suite/test_v3_consumer.py @@ -10,7 +10,7 @@ from pytest_bdd import given, parsers, scenario, then from pact.v3.pact import HttpInteraction, Pact -from tests.v3.compatibility_suite.util import parse_markdown_table +from tests.v3.compatibility_suite.util import PactInteractionTuple, parse_markdown_table from tests.v3.compatibility_suite.util.consumer import ( the_pact_file_for_the_test_is_generated, ) @@ -48,21 +48,21 @@ def test_supports_data_for_provider_states() -> None: target_fixture="pact_interaction", ) def an_integration_is_being_defined_for_a_consumer_test() -> ( - Generator[tuple[Pact, HttpInteraction], Any, None] + Generator[PactInteractionTuple[HttpInteraction], Any, None] ): """An integration is being defined for a consumer test.""" pact = Pact("consumer", "provider") pact.with_specification("V3") - yield (pact, pact.upon_receiving("a request")) + yield PactInteractionTuple(pact, pact.upon_receiving("a request")) @given(parsers.re(r'a provider state "(?P[^"]+)" is specified')) def a_provider_state_is_specified( - pact_interaction: tuple[Pact, HttpInteraction], + pact_interaction: PactInteractionTuple[HttpInteraction], state: str, ) -> None: """A provider state is specified.""" - pact_interaction[1].given(state) + pact_interaction.interaction.given(state) @given( @@ -74,7 +74,7 @@ def a_provider_state_is_specified( converters={"table": parse_markdown_table}, ) def a_provider_state_is_specified_with_the_following_data( - pact_interaction: tuple[Pact, HttpInteraction], + pact_interaction: PactInteractionTuple[HttpInteraction], state: str, table: list[dict[str, Any]], ) -> None: @@ -93,7 +93,7 @@ def a_provider_state_is_specified_with_the_following_data( elif value.replace(".", "", 1).isdigit(): row[key] = float(value) - pact_interaction[1].given(state, parameters=table[0]) + pact_interaction.interaction.given(state, parameters=table[0]) ################################################################################ diff --git a/tests/v3/compatibility_suite/test_v3_message_consumer.py b/tests/v3/compatibility_suite/test_v3_message_consumer.py new file mode 100644 index 000000000..bdb038368 --- /dev/null +++ b/tests/v3/compatibility_suite/test_v3_message_consumer.py @@ -0,0 +1,663 @@ +"""V3 Message consumer feature tests.""" + +from __future__ import annotations + +import ast +import json +import logging +import re +from typing import TYPE_CHECKING, Any, List, NamedTuple + +from pytest_bdd import ( + given, + parsers, + scenario, + then, + when, +) + +from tests.v3.compatibility_suite.util import ( + FIXTURES_ROOT, + PactInteractionTuple, + parse_markdown_table, +) +from tests.v3.compatibility_suite.util.consumer import ( + a_message_integration_is_being_defined_for_a_consumer_test, +) + +if TYPE_CHECKING: + from pathlib import Path + + from pact.v3.pact import AsyncMessageInteraction, InteractionVerificationError + +logger = logging.getLogger(__name__) + +################################################################################ +## Helpers +################################################################################ + + +class ReceivedMessage(NamedTuple): + """Holder class for Message Received Payload.""" + + body: Any + context: Any + + +class PactResult(NamedTuple): + """Holder class for Pact Result objects.""" + + messages: List[ReceivedMessage] + pact_data: dict[str, Any] | None + errors: List[InteractionVerificationError] + + +def assert_type(expected_type: str, value: Any) -> None: # noqa: ANN401 + logger.debug("Ensuring that %s is of type %s", value, expected_type) + if expected_type == "integer": + assert value is not None + assert isinstance(value, int) or re.match(r"^\d+$", value) + else: + msg = f"Unknown type: {expected_type}" + raise ValueError(msg) + + +################################################################################ +## Scenarios +################################################################################ + + +@scenario( + "definition/features/V3/message_consumer.feature", + "When all messages are successfully processed", +) +def test_when_all_messages_are_successfully_processed() -> None: + """When all messages are successfully processed.""" + + +@scenario( + "definition/features/V3/message_consumer.feature", + "When not all messages are successfully processed", +) +def test_when_not_all_messages_are_successfully_processed() -> None: + """When not all messages are successfully processed.""" + + +@scenario( + "definition/features/V3/message_consumer.feature", + "Supports arbitrary message metadata", +) +def test_supports_arbitrary_message_metadata() -> None: + """Supports arbitrary message metadata.""" + + +@scenario( + "definition/features/V3/message_consumer.feature", + "Supports specifying provider states", +) +def test_supports_specifying_provider_states() -> None: + """Supports specifying provider states.""" + + +@scenario( + "definition/features/V3/message_consumer.feature", + "Supports data for provider states", +) +def test_supports_data_for_provider_states() -> None: + """Supports data for provider states.""" + + +@scenario( + "definition/features/V3/message_consumer.feature", + "Supports the use of generators with the message body", +) +def test_supports_the_use_of_generators_with_the_message_body() -> None: + """Supports the use of generators with the message body.""" + + +@scenario( + "definition/features/V3/message_consumer.feature", + "Supports the use of generators with message metadata", +) +def test_supports_the_use_of_generators_with_message_metadata() -> None: + """Supports the use of generators with message metadata.""" + + +################################################################################ +## Given +################################################################################ + + +a_message_integration_is_being_defined_for_a_consumer_test("V3") + + +@given( + parsers.re( + r'a provider state "(?P[^"]+)" for the message is specified' + r"( with the following data:\n)?(?P.*)", + re.DOTALL, + ), + converters={"table": lambda v: parse_markdown_table(v) if v else None}, +) +def a_provider_state_for_the_message_is_specified_with_the_following_data( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + state: str, + table: list[dict[str, str]] | None, +) -> None: + """A provider state for the message is specified with the following data.""" + logger.debug("Specifying provider state '%s': %s", state, table) + if table: + parameters = {k: ast.literal_eval(v) for k, v in table[0].items()} + pact_interaction.interaction.given(state, parameters=parameters) + else: + pact_interaction.interaction.given(state) + + +@given("a message is defined") +def a_message_is_defined() -> None: + """A message is defined.""" + + +@given( + parsers.re( + r"the message contains the following metadata:\n(?P
.+)", + re.DOTALL, + ), + converters={"table": parse_markdown_table}, +) +def the_message_contains_the_following_metadata( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + table: list[dict[str, Any]], +) -> None: + """The message contains the following metadata.""" + logger.debug("Adding metadata to message: %s", table) + for metadata in table: + if metadata.get("value", "").startswith("JSON: "): + metadata["value"] = metadata["value"].replace("JSON:", "") + pact_interaction.interaction.with_metadata({metadata["key"]: metadata["value"]}) + + +@given( + parsers.re( + r"the message is configured with the following:\n(?P
.+)", + re.DOTALL, + ), + converters={"table": parse_markdown_table}, +) +def the_message_is_configured_with_the_following( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + table: list[dict[str, Any]], +) -> None: + """The message is configured with the following.""" + assert len(table) == 1, "Only one row is expected" + config: dict[str, str] = table[0] + + if body := config.pop("body", None): + if body.startswith("file: "): + file = FIXTURES_ROOT / body.replace("file: ", "") + content_type = "application/json" if file.suffix == ".json" else None + pact_interaction.interaction.with_body(file.read_text(), content_type) + else: + msg = f"Unsupported body configuration: {config['body']}" + raise ValueError(msg) + + if generators := config.pop("generators", None): + if generators.startswith("JSON: "): + data = json.loads(generators.replace("JSON: ", "")) + pact_interaction.interaction.with_generators(data) + else: + file = FIXTURES_ROOT / generators + pact_interaction.interaction.with_generators(file.read_text()) + + if metadata := config.pop("metadata", None): + data = json.loads(metadata) + pact_interaction.interaction.with_metadata({ + k: json.dumps(v) for k, v in data.items() + }) + + if config: + msg = f"Unknown configuration keys: {', '.join(config.keys())}" + raise ValueError(msg) + + +@given( + parsers.re(r'the message payload contains the "(?P[^"]+)" JSON document') +) +def the_message_payload_contains_the_basic_json_document( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + basename: str, +) -> None: + """The message payload contains the "basic" JSON document.""" + json_path = FIXTURES_ROOT / f"{basename}.json" + if not json_path.is_file(): + msg = f"File not found: {json_path}" + raise FileNotFoundError(msg) + pact_interaction.interaction.with_body( + json_path.read_text(), + content_type="application/json", + ) + + +################################################################################ +## When +################################################################################ + + +@when("the message is successfully processed", target_fixture="pact_result") +def the_message_is_successfully_processed( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + temp_dir: Path, +) -> PactResult: + """The message is successfully processed.""" + messages: list[ReceivedMessage] = [] + + def handler( + body: str | bytes | None, + context: dict[str, str], + ) -> None: + messages.append(ReceivedMessage(body, context)) + + # While the expectation is that the message will be processed successfully, + # we don't raise an exception and instead capture any errors that occur. + errors = pact_interaction.pact.verify(handler, "Async", raises=False) + if errors: + logger.error("%d errors occured during verification:", len(errors)) + for error in errors: + logger.error(error) + msg = "Errors occurred during verification" + raise AssertionError(msg) + + (temp_dir / "pacts").mkdir(exist_ok=True, parents=True) + pact_interaction.pact.write_file(temp_dir / "pacts") + with (temp_dir / "pacts" / "consumer-provider.json").open() as file: + pact_data = json.load(file) + + return PactResult(messages, pact_data, errors) + + +@when( + parsers.re( + r"the message is NOT successfully processed " + r'with a "(?P[^"]+)" exception' + ), + target_fixture="pact_result", +) +def the_message_is_not_successfully_processed_with_an_exception( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + failure: str, +) -> PactResult: + """The message is NOT successfully processed with a "Test failed" exception.""" + messages: list[ReceivedMessage] = [] + + def handler(body: str | bytes | None, context: dict[str, str]) -> None: + messages.append(ReceivedMessage(body, context)) + raise AssertionError(failure) + + errors = pact_interaction.pact.verify(handler, "Async", raises=False) + return PactResult(messages, None, errors) + + +################################################################################ +## Then +################################################################################ + + +@then( + parsers.re( + r"a Pact file for the message interaction " + r"will(?P( NOT)?) have been written" + ), + converters={"success": lambda x: x != " NOT"}, +) +def a_pact_file_for_the_message_interaction_will_maybe_have_been_written( + temp_dir: Path, + success: bool, # noqa: FBT001 +) -> None: + """A Pact file for the message interaction will maybe have been written.""" + assert (temp_dir / "pacts" / "consumer-provider.json").exists() == success + + +@then(parsers.re(r'the consumer test error will be "(?P[^"]+)"')) +def the_consumer_test_error_will_be_test_failed( + pact_result: PactResult, + error: str, +) -> None: + """The consumer test error will be "Test failed".""" + assert len(pact_result.errors) == 1 + assert error in str(pact_result.errors[0].error) + + +@then( + parsers.re(r"the consumer test will have (?Ppassed|failed)"), + converters={"success": lambda x: x == "passed"}, +) +def the_consumer_test_will_have_passed_or_failed( + pact_result: PactResult, + success: bool, # noqa: FBT001 +) -> None: + """The consumer test will have passed or failed.""" + assert (len(pact_result.errors) == 0) == success + + +@then( + parsers.re( + r"the first message in the pact file content type " + r'will be "(?P[^"]+)"' + ) +) +def the_first_message_in_the_pact_file_content_type_will_be( + pact_result: PactResult, + content_type: str, +) -> None: + """The first message in the pact file content type will be.""" + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages: list[dict[str, dict[str, Any]]] = pact_result.pact_data["messages"] + if not isinstance(messages, list) or not messages: + msg = "No messages found" + raise RuntimeError(msg) + assert messages[0].get("metadata", {}).get("contentType") == content_type + + +@then( + parsers.re( + r"the first message in the pact file will contain " + r"(?P\d+) provider states?" + ), + converters={"state_count": int}, +) +def the_first_message_in_the_pact_file_will_contain( + pact_result: PactResult, + state_count: int, +) -> None: + """The first message in the pact file will contain 1 provider state.""" + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages: list[dict[str, list[Any]]] = pact_result.pact_data["messages"] + if not isinstance(messages, list) or not messages: + msg = "No messages found" + raise RuntimeError(msg) + assert len(messages[0].get("providerStates", [])) == state_count + + +@then( + parsers.re( + r"the first message in the Pact file will contain " + r'provider state "(?P[^"]+)"' + ) +) +def the_first_message_in_the_pact_file_will_contain_provider_state( + pact_result: PactResult, + state: str, +) -> None: + """The first message in the Pact file will contain provider state.""" + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages = pact_result.pact_data["messages"] + if not isinstance(messages, list) or not messages: + msg = "No messages found" + raise RuntimeError(msg) + message: dict[str, Any] = messages[0] + provider_states: list[dict[str, Any]] = message.get("providerStates", []) + for provider_state in provider_states: + if provider_state["name"] == state: + break + else: + msg = f"Provider state not found: {state}" + raise AssertionError(msg) + + +@then( + parsers.re( + r"the first message in the pact file will contain " + r'the "(?P[^"]+)" document' + ) +) +def the_first_message_in_the_pact_file_will_contain_the_basic_json_document( + pact_result: PactResult, + basename: str, +) -> None: + """The first message in the pact file will contain the "basic.json" document.""" + path = FIXTURES_ROOT / basename + if not path.is_file(): + msg = f"File not found: {path}" + raise FileNotFoundError(msg) + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages: list[dict[str, Any]] = pact_result.pact_data["messages"] + if not isinstance(messages, list) or not messages: + msg = "No messages found" + raise RuntimeError(msg) + try: + assert messages[0]["contents"] == json.loads(path.read_text()) + except json.JSONDecodeError as e: + logger.info("Error decoding JSON: %s", e) + logger.info("Performing basic string comparison") + assert messages[0]["contents"] == path.read_text() + + +@then( + parsers.re( + r"the first message in the pact file will contain " + r'the message metadata "(?P[^"]+)" == "(?P[^"\\]*(?:\\.[^"\\]*)*)"' + ) +) +def the_first_message_in_the_pact_file_will_contain_the_message_metadata( + pact_result: PactResult, + key: str, + value: Any, # noqa: ANN401 +) -> None: + """The first message in the pact file will contain the message metadata.""" + if value.startswith("JSON: "): + value = value.replace("JSON: ", "") + value = value.replace('\\"', '"') + value = json.loads(value) + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages: list[dict[str, dict[str, Any]]] = pact_result.pact_data["messages"] + assert messages[0]["metadata"][key] == value + + +@then( + parsers.re( + r'the message contents for "(?P[^"]+)" ' + r'will have been replaced with an? "(?P[^"]+)"' + ) +) +def the_message_contents_will_have_been_replaced_with( + pact_result: PactResult, + path: str, + expected_type: str, +) -> None: + """The message contents for "$.one" will have been replaced with an "integer".""" + json_path = path.split(".") + assert len(json_path) == 2, "Only one level of nesting is supported" + assert json_path[0] == "$", "Only root level replacement is supported" + key = json_path[1] + + assert len(pact_result.messages) == 1 + message = pact_result.messages[0] + value = json.loads(message.body).get(key) + assert_type(expected_type, value) + + +@then( + parsers.parse( + "the pact file will contain {interaction_count:d} message interaction" + ) +) +def the_pact_file_will_contain_message_interaction( + pact_result: PactResult, + interaction_count: int, +) -> None: + """The pact file will contain N message interaction.""" + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages: list[Any] = pact_result.pact_data["messages"] + assert len(messages) == interaction_count + + +@then( + parsers.re( + r'the provider state "(?P[^"]+)" for the message ' + r"will contain the following parameters:\n(?P
.+)", + re.DOTALL, + ), + converters={"table": parse_markdown_table}, +) +def the_provider_state_for_the_message_will_contain_the_following_parameters( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + pact_result: PactResult, + state: str, + table: list[dict[str, Any]], +) -> None: + """The provider state for the message will contain the following parameters.""" + assert len(table) == 1, "Only one row is expected" + expected = json.loads(table[0]["parameters"]) + + # It is unclear whether this test is meant to verify the `Interaction` + # object, or the result as written to the Pact file. As a result, we + # will perform both checks. + + ## Verifying the Pact File + + if not pact_result.pact_data: + msg = "No pact data found" + raise RuntimeError(msg) + messages: list[dict[str, list[dict[str, Any]]]] = pact_result.pact_data["messages"] + assert len(messages) == 1, "Only one message is expected" + message = messages[0] + + assert len(message["providerStates"]) > 0, "At least one provider state is expected" + provider_states = message["providerStates"] + for provider_state_dict in provider_states: + if provider_state_dict["name"] == state: + assert expected == provider_state_dict["params"] + break + else: + msg = f"Provider state not found in Pact file: {state}" + raise AssertionError(msg) + + ## Verifying the Interaction Object + + for interaction in pact_interaction.pact.interactions("Async"): + for provider_state in interaction.provider_states(): + if provider_state.name == state: + provider_state_params = { + k: ast.literal_eval(v) for k, v in provider_state.parameters() + } + assert expected == provider_state_params + break + else: + msg = f"Provider state not found: {state}" + raise ValueError(msg) + break + else: + msg = "No interactions found" + raise ValueError(msg) + + +@then( + parsers.re(r'the received message content type will be "(?P[^"]+)"') +) +def the_received_message_content_type_will_be( + pact_result: PactResult, + content_type: str, +) -> None: + """The received message content type will be "application/json".""" + assert len(pact_result.messages) == 1 + message = pact_result.messages[0] + assert message.context.get("contentType") == content_type + + +@then( + parsers.re( + r"the received message metadata will contain " + r'"(?P[^"]+)" == "(?P[^"\\]*(?:\\.[^"\\]*)*)"' + ) +) +def the_received_message_metadata_will_contain( + pact_result: PactResult, + key: str, + value: Any, # noqa: ANN401 +) -> None: + """The received message metadata will contain.""" + # If we're given some JSON value, we will need to parse the value from the + # `message.context` and compare it to the parsed JSON value; otherwise, + # equivalent JSON values may not match due to formatting differences. + json_matching = False + if value.startswith("JSON: "): + value = value.replace("JSON: ", "").replace(r"\"", '"') + value = json.loads(value) + json_matching = True + + assert len(pact_result.messages) == 1 + message = pact_result.messages[0] + for k, v in message.context.items(): + if k == key: + if json_matching: + assert json.loads(v) == value + else: + assert v == value + break + else: + msg = f"Key '{key}' not found in message metadata" + raise AssertionError(msg) + + +@then( + parsers.re( + r'the received message metadata will contain "(?P[^"]+)" ' + r'replaced with an? "(?P[^"]+)"' + ) +) +def the_received_message_metadata_will_contain_replaced_with( + pact_result: PactResult, + key: str, + expected_type: str, +) -> None: + """The received message metadata will contain "ID" replaced with an "integer".""" + assert isinstance(pact_result.messages, list) + assert len(pact_result.messages) == 1, "Only one message is expected" + message = pact_result.messages[0] + value = message.context.get(key) + assert_type(expected_type, value) + + +@then( + parsers.re( + r"the received message payload will contain " + r'the "(?P[^"]+)" JSON document' + ) +) +def the_received_message_payload_will_contain_the_basic_json_document( + pact_result: PactResult, + basename: str, +) -> None: + """The received message payload will contain the JSON document.""" + json_path = FIXTURES_ROOT / f"{basename}.json" + if not json_path.is_file(): + msg = f"File not found: {json_path}" + raise FileNotFoundError(msg) + + assert len(pact_result.messages) == 1 + message = pact_result.messages[0] + + try: + assert json.loads(message.body) == json.loads(json_path.read_text()) + except json.JSONDecodeError as e: + logger.info("Error decoding JSON: %s", e) + logger.info("Performing basic comparison") + if isinstance(message.body, str): + assert message.body == json_path.read_text() + elif isinstance(message.body, bytes): + assert message.body == json_path.read_bytes() + else: + msg = f"Unexpected message body type: {type(message.body).__name__}" + raise TypeError(msg) from None diff --git a/tests/v3/compatibility_suite/test_v4_consumer.py b/tests/v3/compatibility_suite/test_v4_consumer.py index 7b7ab019d..de70ea6f0 100644 --- a/tests/v3/compatibility_suite/test_v4_consumer.py +++ b/tests/v3/compatibility_suite/test_v4_consumer.py @@ -9,7 +9,7 @@ from pytest_bdd import given, parsers, scenario, then from pact.v3.pact import HttpInteraction, Pact -from tests.v3.compatibility_suite.util import string_to_int +from tests.v3.compatibility_suite.util import PactInteractionTuple, string_to_int from tests.v3.compatibility_suite.util.consumer import ( the_pact_file_for_the_test_is_generated, ) @@ -63,41 +63,38 @@ def test_supports_adding_comments() -> None: target_fixture="pact_interaction", ) def an_http_interaction_is_being_defined_for_a_consumer_test() -> ( - Generator[tuple[Pact, HttpInteraction], Any, None] + Generator[PactInteractionTuple[HttpInteraction], Any, None] ): """An HTTP interaction is being defined for a consumer test.""" pact = Pact("consumer", "provider") pact.with_specification("V4") - yield (pact, pact.upon_receiving("a request")) + yield PactInteractionTuple(pact, pact.upon_receiving("a request")) @given(parsers.re(r'a key of "(?P[^"]+)" is specified for the HTTP interaction')) def a_key_is_specified_for_the_http_interaction( - pact_interaction: tuple[Pact, HttpInteraction], + pact_interaction: PactInteractionTuple[HttpInteraction], key: str, ) -> None: """A key is specified for the HTTP interaction.""" - _, interaction = pact_interaction - interaction.set_key(key) + pact_interaction.interaction.set_key(key) @given("the HTTP interaction is marked as pending") def the_http_interaction_is_marked_as_pending( - pact_interaction: tuple[Pact, HttpInteraction], + pact_interaction: PactInteractionTuple[HttpInteraction], ) -> None: """The HTTP interaction is marked as pending.""" - _, interaction = pact_interaction - interaction.set_pending(pending=True) + pact_interaction.interaction.set_pending(pending=True) @given(parsers.re(r'a comment "(?P[^"]+)" is added to the HTTP interaction')) def a_comment_is_added_to_the_http_interaction( - pact_interaction: tuple[Pact, HttpInteraction], + pact_interaction: PactInteractionTuple[HttpInteraction], comment: str, ) -> None: """A comment of "" is added to the HTTP interaction.""" - _, interaction = pact_interaction - interaction.set_comment("text", [comment]) + pact_interaction.interaction.set_comment("text", [comment]) ################################################################################ diff --git a/tests/v3/compatibility_suite/test_v4_message_consumer.py b/tests/v3/compatibility_suite/test_v4_message_consumer.py new file mode 100644 index 000000000..5b1332165 --- /dev/null +++ b/tests/v3/compatibility_suite/test_v4_message_consumer.py @@ -0,0 +1,148 @@ +"""Message consumer feature tests.""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING, Any + +from pytest_bdd import ( + given, + parsers, + scenario, + then, +) + +from tests.v3.compatibility_suite.util import PactInteractionTuple, string_to_int +from tests.v3.compatibility_suite.util.consumer import ( + a_message_integration_is_being_defined_for_a_consumer_test, + the_pact_file_for_the_test_is_generated, +) + +if TYPE_CHECKING: + from pact.v3.pact import AsyncMessageInteraction + + +@scenario( + "definition/features/V4/message_consumer.feature", + "Sets the type for the interaction", +) +def test_sets_the_type_for_the_interaction() -> None: + """Sets the type for the interaction.""" + + +@scenario( + "definition/features/V4/message_consumer.feature", + "Supports specifying a key for the interaction", +) +def test_supports_specifying_a_key_for_the_interaction() -> None: + """Supports specifying a key for the interaction.""" + + +@scenario( + "definition/features/V4/message_consumer.feature", + "Supports specifying the interaction is pending", +) +def test_supports_specifying_the_interaction_is_pending() -> None: + """Supports specifying the interaction is pending.""" + + +@scenario( + "definition/features/V4/message_consumer.feature", + "Supports adding comments", +) +def test_supports_adding_comments() -> None: + """Supports adding comments.""" + + +################################################################################ +## Given +################################################################################ + +a_message_integration_is_being_defined_for_a_consumer_test("V4") + + +@given( + parsers.re(r'a comment "(?P[^"]+)" is added to the message interaction') +) +def a_comment_is_added_to_the_message_interaction( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + comment: str, +) -> None: + """A comment "{comment}" is added to the message interaction.""" + pact_interaction.interaction.add_text_comment(comment) + + +@given( + parsers.re(r'a key of "(?P[^"]+)" is specified for the message interaction') +) +def a_key_is_specified_for_the_message_interaction( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], + key: str, +) -> None: + """A key is specified for the HTTP interaction.""" + pact_interaction.interaction.set_key(key) + + +@given("the message interaction is marked as pending") +def the_message_interaction_is_marked_as_pending( + pact_interaction: PactInteractionTuple[AsyncMessageInteraction], +) -> None: + """The message interaction is marked as pending.""" + pact_interaction.interaction.set_pending(pending=True) + + +################################################################################ +## When +################################################################################ + + +the_pact_file_for_the_test_is_generated() + + +################################################################################ +## Then +################################################################################ + + +@then( + parsers.re( + r"the (?P[^ ]+) interaction in the Pact file" + r" will have \"(?P[^\"]+)\" = '(?P[^']+)'" + ), + converters={"num": string_to_int}, +) +def the_interaction_in_the_pact_file_will_have_a_key_of( + pact_data: dict[str, Any], + num: int, + key: str, + value: str, +) -> None: + """The interaction in the Pact file will have a key of value.""" + assert "interactions" in pact_data + assert len(pact_data["interactions"]) >= num + interaction = pact_data["interactions"][num - 1] + assert key in interaction + value = json.loads(value) + if isinstance(value, list): + assert interaction[key] in value + else: + assert interaction[key] == value + + +@then( + parsers.re( + r"the (?P[^ ]+) interaction in the Pact file" + r' will have a type of "(?P[^"]+)"' + ), + converters={"num": string_to_int}, +) +def the_interaction_in_the_pact_file_will_container_provider_states( + pact_data: dict[str, Any], + num: int, + interaction_type: str, +) -> None: + """The interaction in the Pact file will container provider states.""" + assert "interactions" in pact_data + assert len(pact_data["interactions"]) >= num + interaction = pact_data["interactions"][num - 1] + assert interaction["type"] == interaction_type diff --git a/tests/v3/compatibility_suite/util/__init__.py b/tests/v3/compatibility_suite/util/__init__.py index 1351b1146..91b5871d1 100644 --- a/tests/v3/compatibility_suite/util/__init__.py +++ b/tests/v3/compatibility_suite/util/__init__.py @@ -31,7 +31,7 @@ def _(): from collections.abc import Collection, Mapping from datetime import date, datetime, time from pathlib import Path -from typing import Any +from typing import Any, Generic, TypeVar from xml.etree import ElementTree import flask @@ -47,6 +47,34 @@ def _(): SUITE_ROOT = Path(__file__).parent.parent / "definition" FIXTURES_ROOT = SUITE_ROOT / "fixtures" +_T = TypeVar("_T") + + +class PactInteractionTuple(Generic[_T]): + """ + Pact and interaction tuple. + + A number of steps in the compatibility suite require one or both of a `Pact` + and an `Interaction` subclass. This named tuple is used to pass these + objects around more easily. + + !!! note + + This should be simplified in the future to simply being a + [`NamedTuple`][typing.NamedTuple]; however, earlier versions of Python + do not support inheriting from multiple classes, thereby preventing + `class PactInteractionTuple(NamedTuple, Generic[_T])` (even if + [`Generic[_T]`][typing.Generic] serves no purpose other than to allow + type hinting). + """ + + def __init__(self, pact: Pact, interaction: _T) -> None: + """ + Instantiate the tuple. + """ + self.pact = pact + self.interaction = interaction + def string_to_int(word: str) -> int: """ diff --git a/tests/v3/compatibility_suite/util/consumer.py b/tests/v3/compatibility_suite/util/consumer.py index f9834187d..38bf7676e 100644 --- a/tests/v3/compatibility_suite/util/consumer.py +++ b/tests/v3/compatibility_suite/util/consumer.py @@ -11,12 +11,13 @@ import pytest import requests -from pytest_bdd import parsers, then, when +from pytest_bdd import given, parsers, then, when from yarl import URL from pact.v3 import Pact from tests.v3.compatibility_suite.util import ( FIXTURES_ROOT, + PactInteractionTuple, parse_markdown_table, string_to_int, truncate, @@ -26,11 +27,41 @@ from collections.abc import Generator from pathlib import Path - from pact.v3.pact import HttpInteraction, PactServer + from pact.v3.interaction._async_message_interaction import AsyncMessageInteraction + from pact.v3.pact import PactServer from tests.v3.compatibility_suite.util import InteractionDefinition logger = logging.getLogger(__name__) +################################################################################ +## Given +################################################################################ + + +def a_message_integration_is_being_defined_for_a_consumer_test( + version: str, + stacklevel: int = 1, +) -> None: + @given( + parsers.re( + r"a message (integration|interaction) " + r"is being defined for a consumer test" + ), + target_fixture="pact_interaction", + stacklevel=stacklevel + 1, + ) + def _() -> PactInteractionTuple[AsyncMessageInteraction]: + """ + A message integration is being defined for a consumer test. + """ + pact = Pact("consumer", "provider") + pact.with_specification(version) + return PactInteractionTuple( + pact, + pact.upon_receiving("an asynchronous message", "Async"), + ) + + ################################################################################ ## When ################################################################################ @@ -201,10 +232,10 @@ def the_pact_file_for_the_test_is_generated(stacklevel: int = 1) -> None: ) def _( temp_dir: Path, - pact_interaction: tuple[Pact, HttpInteraction], + pact_interaction: PactInteractionTuple[Any], ) -> dict[str, Any]: """The Pact file for the test is generated.""" - pact_interaction[0].write_file(temp_dir) + pact_interaction.pact.write_file(temp_dir) with (temp_dir / "consumer-provider.json").open("r") as file: return json.load(file) diff --git a/tests/v3/test_pact.py b/tests/v3/test_pact.py index 5f32e7d80..2264346d0 100644 --- a/tests/v3/test_pact.py +++ b/tests/v3/test_pact.py @@ -93,16 +93,6 @@ def test_interactions_iter( raise RuntimeError(msg) -def test_messages(pact: Pact) -> None: - messages = pact.messages() - assert messages is not None - for _message in messages: - # This should be an empty list and therefore the error should never be - # raised. - msg = "Should not be reached" - raise RuntimeError(msg) - - def test_write_file(pact: Pact, temp_dir: Path) -> None: pact.write_file(temp_dir) outfile = temp_dir / "consumer-provider.json"