Skip to content

Commit 609d814

Browse files
committed
docs(blog): add functional arguments post
Signed-off-by: JP-Ellis <josh@jpellis.me>
1 parent 6c56073 commit 609d814

File tree

1 file changed

+306
-0
lines changed

1 file changed

+306
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
---
2+
authors:
3+
- JP-Ellis
4+
date:
5+
created: 2024-12-30
6+
---
7+
8+
# Functional Arguments
9+
10+
Today marks the [release of Pact Python version 2.3.0](https://github.com/pact-foundation/pact-python/releases/tag/v2.3.0). Among the many incremental improvements, the most significant is the [support of functional arguments](https://github.com/pact-foundation/pact-python/pull/890). This feature provides an improved user experience for providers, and also introduced a couple of breaking changes to the `pact.v3` preview.
11+
12+
If you just want to update your existing code to the latest version without any other changes, you can skip to the [Breaking Changes TL;DR](#breaking-changes-tldr) section. Otherwise, the key new features allow you to [define provider states using functions](#functional-state-handler) and [use functions to produce messages](#functional-message-producer).
13+
<!-- more -->
14+
15+
## Breaking Changes TL;DR
16+
17+
While I highly recommend everyone to experiment with the new possibilities that functional arguments bring, if you merely want to update your existing code to the latest version without any other changes, here is a quick summary of the breaking changes:
18+
19+
- The `Verifier` initialization now requires a `name` argument which is used to identify the provider in the Pact file. This information was previously given through the `set_info` method which has been removed. The change required is:
20+
21+
<!-- markdownlint-disable code-block-style -->
22+
=== "Before"
23+
24+
```python
25+
verifier = Verifier()
26+
verifier.set_info("provider_name", ...)
27+
```
28+
29+
=== "After"
30+
31+
```python
32+
verifier = Verifier(name="provider_name")
33+
```
34+
<!-- markdownlint-enable code-block-style -->
35+
36+
- The `Verifier.set_info` method has been entirely removed. Instead, the `Verifier` class now has a `name` attribute which is set during initialization for the provider's name, and the transport information that was previously set is now passed through the `add_transport` method:
37+
38+
<!-- markdownlint-disable code-block-style -->
39+
=== "Before"
40+
41+
```python
42+
verifier = Verifier()
43+
verifier.set_info(
44+
"provider_name",
45+
url="http://localhost:8123",
46+
)
47+
```
48+
49+
=== "After"
50+
51+
```python
52+
verifier = Verifier("provider_name")
53+
verifier.add_transport(url="http://localhost:8123")
54+
```
55+
<!-- markdownlint-enable code-block-style -->
56+
57+
- The `Verifier.set_state` function has been renamed to `Verifier.state_handler`. Furthermore, if you have already set up a custom endpoint to handle provider state changes, you will now need to explicitly state whether your endpoint expects data to be passed through the query string or through a post body:
58+
59+
<!-- markdownlint-disable code-block-style -->
60+
=== "Before"
61+
62+
```python
63+
verifier = Verifier()
64+
verifier.set_state("http://localhost:8123/provider-states")
65+
```
66+
67+
=== "After"
68+
69+
```python
70+
verifier = Verifier()
71+
verifier.state_handler(
72+
"http://localhost:8123/provider-states",
73+
body=False, # the previous default must be explicitly set
74+
)
75+
```
76+
<!-- markdownlint-enable code-block-style -->
77+
78+
## Functional State Handler
79+
80+
When a Pact interaction is to be verified, the consumer will often expect the provider to be in a particular state. For example, a consumer might want to fetch a specific user's details, and therefore the provider must be in a state where that user exists. The user experience prior to version 2.3.0 was less than ideal: the developers behind the provider had to set up a custom endpoint to handle the state changes, and then pass the URL of that endpoint to the `Verifier` object.
81+
82+
The new `state_handler` method replaces the `set_state` method and simplifies this process significantly by allowing functions to be called to set up and tear down the provider state. For example, the following code snippet demonstrates how to set up a state handler that uses a custom endpoint to handle the provider state:
83+
84+
<!-- markdownlint-disable code-block-style -->
85+
???+ example
86+
87+
```python
88+
from pact.v3 import Verifier
89+
90+
def provider_state_callback(
91+
name: str, # (1)
92+
action: Literal["setup", "teardown"], # (2)
93+
params: dict[str, Any] | None, # (3)
94+
) -> None:
95+
"""
96+
Callback to set up and tear down the provider state.
97+
98+
Args:
99+
name:
100+
The name of the provider state. For example, "a user with ID 123
101+
exists" or "no users exist".
102+
103+
action:
104+
The action to perform. Either "setup" or "teardown". The setup
105+
action should create the provider state, and the teardown action
106+
should remove it.
107+
108+
params:
109+
If the provider state has additional parameters, they will be
110+
passed here. For example, instead of "a user with ID 123 exists",
111+
the provider state might be "a user with the given ID exists" and
112+
the specific ID would be passed in the params.
113+
"""
114+
...
115+
116+
def test_provider():
117+
verifier = Verifier("provider_name")
118+
verifier.state_handler(provider_state_callback, teardown=True)
119+
```
120+
121+
1. The `name` parameter is the name of the provider state. For example, "a user with ID 123 exists" or "no users exist". If you instead use a mapping of provider state names to functions, this parameter is not passed to the function.
122+
2. The `action` parameter is either "setup" or "teardown". The setup action should create the provider state, and the teardown action should remove it. If you specify `teardown=False`, then the `action` parameter is _not_ passed to the callback function.
123+
3. The `params` parameter is a dictionary of additional parameters that the provider state requires. For example, instead of "a user with ID 123 exists", the provider state might be "a user with the given ID exists" and the specific ID would be passed in the `params` dictionary. Note that `params` is always present, but may be `None` if no parameters are specified by the consumer.
124+
<!-- markdownlint-enable code-block-style -->
125+
126+
This snippet showcases the most flexible way to set up the provider state with a function that is fully parameterized. The `state_handler` method also handles the following scenarios:
127+
128+
- If teardowns are never required, then one should specify `teardown=False` in which case the `action` parameter is _not_ passed to the callback function.
129+
130+
<!-- markdownlint-disable code-block-style -->
131+
??? example
132+
133+
```python
134+
from pact.v3 import Verifier
135+
136+
def provider_state_callback(
137+
name: str,
138+
params: dict[str, Any] | None,
139+
) -> None:
140+
...
141+
142+
def test_provider():
143+
verifier = Verifier("provider_name")
144+
verifier.state_handler(provider_state_callback, teardown=False)
145+
```
146+
<!-- markdownlint-enable code-block-style -->
147+
148+
- Instead of the function performing the mapping of name to necessary action, a mapping can be provided to the `state_handler` method with keys as the provider state names and values as the function to call. Note that if combined with the `teardown=False` option, the function should expect only expect one argument: the `params` dictionary (which itself may be `None`).
149+
150+
<!-- markdownlint-disable code-block-style -->
151+
??? example
152+
153+
```python
154+
from pact.v3 import Verifier
155+
156+
def user_state_callback(
157+
action: Literal["setup", "teardown"],
158+
params: dict[str, Any] | None,
159+
) -> None:
160+
...
161+
162+
def no_users_state_callback(
163+
action: Literal["setup", "teardown"],
164+
params: dict[str, Any] | None,
165+
) -> None:
166+
...
167+
168+
def test_provider():
169+
verifier = Verifier("provider_name")
170+
verifier.state_handler(
171+
{
172+
"a user with ID 123 exists": user_state_callback,
173+
"no users exist": no_users_state_callback,
174+
},
175+
)
176+
```
177+
<!-- markdownlint-enable code-block-style -->
178+
179+
- Both scenarios can be combined, in which a mapping of provide state names to functions is provided, and the `teardown=False` option is specified. In this case, the function should expect only one argument: the `params` dictionary (which itself may be `None`).
180+
181+
<!-- markdownlint-disable code-block-style -->
182+
??? example
183+
184+
```python
185+
from pact.v3 import Verifier
186+
187+
def user_state_callback(
188+
params: dict[str, Any] | None,
189+
) -> None:
190+
...
191+
192+
def no_users_state_callback(
193+
params: dict[str, Any] | None,
194+
) -> None:
195+
...
196+
197+
def test_provider():
198+
verifier = Verifier("provider_name")
199+
verifier.state_handler(
200+
{
201+
"a user with ID 123 exists": user_state_callback,
202+
"no users exist": no_users_state_callback,
203+
},
204+
teardown=False,
205+
)
206+
```
207+
208+
## Functional Message Producer
209+
210+
In the messaging paradigm, the Pact consumer consumes the message produced by the provider (which is often referred to as the "producer"). As there are many and varied transport mechanisms for messages, Pact approaches the verification of messages in a transport-agnostic way. Previously, the provider would need to define a special HTTP endpoint to generate the message, and then pass the URL of that endpoint to the `Verifier` object. This process was cumbersome, especially considering that most producers do not expose any HTTP endpoints to begin with.
211+
212+
With the update to 2.3.0, the `Verifier` class has a new `message_handler` method which allows the provider to pass a function that generates the message. This function is called by the `Verifier` object when it needs a message to verify. The following code snippet demonstrates how to set up a message producer that uses a custom endpoint to generate the message:
213+
214+
<!-- markdownlint-disable code-block-style -->
215+
???+ example
216+
217+
```python
218+
from pact.v3 import Verifier
219+
from pact.v3.types import Message
220+
221+
def message_producer_callback(
222+
name: str, # (1)
223+
params: dict[str, Any] | None, # (2)
224+
) -> Message:
225+
"""
226+
Callback to produce the message that the consumer expects.
227+
228+
Args:
229+
name:
230+
The name of the message. For example "request to delete a user".
231+
232+
params:
233+
If the message has additional parameters, they will be passed
234+
here. For example, one could specify the user ID to delete in
235+
the parameters instead of the message.
236+
237+
Returns:
238+
The message that the consumer expects.
239+
"""
240+
...
241+
242+
def test_provider():
243+
verifier = Verifier("provider_name")
244+
verifier.message_handler(message_producer_callback)
245+
```
246+
247+
1. The `name` parameter is the name of the message. For example, "request to delete a user". If you instead use a mapping of message names to functions, this parameter is not passed to the function.
248+
2. The `params` parameter is a dictionary of additional parameters that the message requires. For example, one could specify the user ID to delete in the parameters instead of the message. Note that `params` is always present, but may be `None` if no parameters are specified by the consumer.
249+
<!-- markdownlint-enable code-block-style -->
250+
251+
The output of the callback function should be an instance of the `Message` type. This is a simple [TypedDict][typing.TypedDict] that represents the message that the consumer expects and can be specified as a simple dictionary, or with typing hints through the `Message` constructor:
252+
253+
<!-- markdownlint-disable code-block-style -->
254+
=== "With typing hints"
255+
256+
```python
257+
from pact.v3.types import Message
258+
259+
message = Message(
260+
contents=json.dumps({
261+
"action": "delete_user",
262+
"user_id": "123",
263+
}).encode("utf-8"),
264+
metadata=None,
265+
content_type="application/json",
266+
)
267+
```
268+
269+
=== "Without typing hints"
270+
271+
```python
272+
message = {
273+
"contents": json.dumps({
274+
"action": "delete_user",
275+
"user_id": "123",
276+
}).encode("utf-8"),
277+
"metadata": None,
278+
"content_type": "application/json",
279+
}
280+
```
281+
282+
In much the same way as the `state_handler` method, the `message_handler` method can also accept a mapping of message names to functions or raw messages. The function should expect only one argument: the `params` dictionary (which itself may be `None`); or if the message is static, the message can be provided directly:
283+
284+
<!-- markdownlint-disable code-block-style -->
285+
???+ example
286+
287+
```python
288+
from pact.v3 import Verifier
289+
from pact.v3.types import Message
290+
291+
def delete_user_message(params: dict[str, Any] | None) -> Message:
292+
...
293+
294+
def test_provider():
295+
verifier = Verifier("provider_name")
296+
verifier.message_handler(
297+
{
298+
"request to delete a user": delete_user_message,
299+
"create user": {
300+
"contents": b"some message",
301+
"metadata": None,
302+
"content_type": "text/plain",
303+
},
304+
},
305+
)
306+
```

0 commit comments

Comments
 (0)