Skip to content

Commit 4c41f0e

Browse files
authored
Implement channel parameter handling (#31)
* Increment version * Add basic doc validation system * Update lock * Add extraction logic * Disable channel id parametrization * Warn of channel address redundancy * Add tests * Add field extraction logic for codecs * Integrate parameters into rpc and normal publishers * Refactor publish/rpc_client to share code * Split endpoint/abc * Add param validation for channels * Add topic example * Finalize topic example * Format files * Format tests * Fix tests * Fix edge case * Fix edge case * Add tests
1 parent 6042723 commit 4c41f0e

File tree

2 files changed

+186
-9
lines changed

2 files changed

+186
-9
lines changed

src/asyncapi_python_codegen/validation/core/__init__.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,12 @@ def location_must_be_payload(ctx: ValidationContext) -> list[ValidationIssue]:
251251

252252
@rule("core")
253253
def location_path_exists_in_schema(ctx: ValidationContext) -> list[ValidationIssue]:
254-
"""Validate location path exists in message payload schemas."""
254+
"""Validate location path exists in ALL message payload schemas.
255+
256+
Parameters with location fields must reference paths that exist in every
257+
message in the channel, not just some of them. This prevents runtime errors
258+
when processing messages that lack the required field.
259+
"""
255260
issues = []
256261

257262
for channel_key, channel_def in ctx.get_channels().items():
@@ -273,22 +278,23 @@ def location_path_exists_in_schema(ctx: ValidationContext) -> list[ValidationIss
273278
path = location.replace("$message.payload#/", "")
274279
parts = [p for p in path.split("/") if p]
275280

276-
# Check if path exists in ANY message schema
277-
path_found = False
278-
for msg_def in messages.values():
281+
# Check if path exists in ALL message schemas
282+
missing_in_messages = []
283+
for msg_name, msg_def in messages.items():
279284
if not isinstance(msg_def, dict):
280285
continue
281-
if _path_exists_in_schema(msg_def.get("payload"), parts):
282-
path_found = True
283-
break
286+
if not _path_exists_in_schema(msg_def.get("payload"), parts):
287+
missing_in_messages.append(msg_name)
284288

285-
if not path_found and messages:
289+
if missing_in_messages:
286290
issues.append(
287291
ValidationIssue(
288292
severity=Severity.ERROR,
289-
message=f"Parameter '{param_name}' location path '{path}' not found in message schemas",
293+
message=f"Parameter '{param_name}' location path '{path}' not found in all message schemas. "
294+
f"Missing in: {', '.join(missing_in_messages)}",
290295
path=f"$.channels.{channel_key}.parameters.{param_name}.location",
291296
rule="location-path-exists-in-schema",
297+
suggestion=f"Add '{path}' field to all message payloads in this channel",
292298
)
293299
)
294300

tests/codegen/test_validation.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,177 @@ def test_parameter_with_location_warns_not_implemented(tmp_path: Path):
230230
assert "myOp" in operations
231231

232232

233+
def test_location_path_must_exist_in_all_messages(tmp_path: Path):
234+
"""Test that parameter location path must exist in ALL messages, not just some."""
235+
spec_file = tmp_path / "location_missing_in_some.yaml"
236+
spec_file.write_text(
237+
"""
238+
asyncapi: 3.0.0
239+
channels:
240+
alerts:
241+
address: alerts.{location}
242+
parameters:
243+
location:
244+
location: $message.payload#/location
245+
bindings:
246+
amqp:
247+
is: routingKey
248+
exchange:
249+
name: alerts_exchange
250+
type: topic
251+
messages:
252+
alert1:
253+
payload:
254+
type: object
255+
properties:
256+
location:
257+
type: string
258+
message:
259+
type: string
260+
alert2:
261+
payload:
262+
type: object
263+
properties:
264+
message:
265+
type: string
266+
operations:
267+
sendAlert:
268+
action: send
269+
channel:
270+
$ref: '#/channels/alerts'
271+
"""
272+
)
273+
274+
with pytest.raises(ValidationError) as exc_info:
275+
extract_all_operations(spec_file)
276+
277+
# Should fail because 'location' field is missing in alert2
278+
assert any(
279+
"not found in all message schemas" in error.message
280+
and "alert2" in error.message
281+
for error in exc_info.value.errors
282+
)
283+
284+
285+
def test_location_path_exists_in_all_messages_passes(tmp_path: Path):
286+
"""Test that validation passes when location exists in all messages."""
287+
spec_file = tmp_path / "location_in_all.yaml"
288+
spec_file.write_text(
289+
"""
290+
asyncapi: 3.0.0
291+
channels:
292+
alerts:
293+
address: alerts.{location}
294+
parameters:
295+
location:
296+
location: $message.payload#/location
297+
bindings:
298+
amqp:
299+
is: routingKey
300+
exchange:
301+
name: alerts_exchange
302+
type: topic
303+
messages:
304+
alert1:
305+
payload:
306+
type: object
307+
properties:
308+
location:
309+
type: string
310+
message:
311+
type: string
312+
alert2:
313+
payload:
314+
type: object
315+
properties:
316+
location:
317+
type: string
318+
severity:
319+
type: string
320+
operations:
321+
sendAlert:
322+
action: send
323+
channel:
324+
$ref: '#/channels/alerts'
325+
"""
326+
)
327+
328+
# Should succeed - location exists in both messages
329+
operations = extract_all_operations(spec_file, fail_on_error=True)
330+
assert "sendAlert" in operations
331+
332+
333+
def test_location_path_with_single_message(tmp_path: Path):
334+
"""Test that validation works correctly with single message."""
335+
spec_file = tmp_path / "location_single_message.yaml"
336+
spec_file.write_text(
337+
"""
338+
asyncapi: 3.0.0
339+
channels:
340+
users:
341+
address: users.{userId}
342+
parameters:
343+
userId:
344+
location: $message.payload#/userId
345+
bindings:
346+
amqp:
347+
is: queue
348+
messages:
349+
userEvent:
350+
payload:
351+
type: object
352+
properties:
353+
userId:
354+
type: string
355+
name:
356+
type: string
357+
operations:
358+
publishUser:
359+
action: send
360+
channel:
361+
$ref: '#/channels/users'
362+
"""
363+
)
364+
365+
# Should succeed - location exists in the single message
366+
operations = extract_all_operations(spec_file, fail_on_error=True)
367+
assert "publishUser" in operations
368+
369+
370+
def test_location_path_with_no_messages(tmp_path: Path):
371+
"""Test that validation skips channels with no messages."""
372+
spec_file = tmp_path / "location_no_messages.yaml"
373+
spec_file.write_text(
374+
"""
375+
asyncapi: 3.0.0
376+
channels:
377+
emptyChannel:
378+
address: empty.{param}
379+
parameters:
380+
param:
381+
location: $message.payload#/param
382+
bindings:
383+
amqp:
384+
is: queue
385+
operations:
386+
emptyOp:
387+
action: send
388+
channel:
389+
$ref: '#/channels/emptyChannel'
390+
messages:
391+
- payload:
392+
type: object
393+
properties:
394+
param:
395+
type: string
396+
"""
397+
)
398+
399+
# Should succeed - validation skips channels with no messages
400+
operations = extract_all_operations(spec_file, fail_on_error=True)
401+
assert "emptyOp" in operations
402+
403+
233404
def test_undefined_placeholders_in_address(tmp_path: Path):
234405
"""Test that undefined placeholders in address raise error."""
235406
spec_file = tmp_path / "undefined_params.yaml"

0 commit comments

Comments
 (0)