xcore-protocol 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xcore_protocol-0.5.1/PKG-INFO +92 -0
- xcore_protocol-0.5.1/README.md +79 -0
- xcore_protocol-0.5.1/pyproject.toml +32 -0
- xcore_protocol-0.5.1/python/tests/test_canonical_protocol_fixtures.py +171 -0
- xcore_protocol-0.5.1/python/tests/test_chat_fixtures.py +145 -0
- xcore_protocol-0.5.1/python/tests/test_discord_fixtures.py +87 -0
- xcore_protocol-0.5.1/python/tests/test_envelope_schemas.py +396 -0
- xcore_protocol-0.5.1/python/tests/test_generated_chat_models.py +251 -0
- xcore_protocol-0.5.1/python/tests/test_generated_discord_models.py +123 -0
- xcore_protocol-0.5.1/python/tests/test_generated_maps_models.py +120 -0
- xcore_protocol-0.5.1/python/tests/test_generated_moderation_models.py +223 -0
- xcore_protocol-0.5.1/python/tests/test_generated_telemetry_models.py +32 -0
- xcore_protocol-0.5.1/python/tests/test_generator_bootstrap.py +121 -0
- xcore_protocol-0.5.1/python/tests/test_java_python_route_compatibility.py +125 -0
- xcore_protocol-0.5.1/python/tests/test_java_python_serialized_compatibility.py +68 -0
- xcore_protocol-0.5.1/python/tests/test_maps_fixtures.py +81 -0
- xcore_protocol-0.5.1/python/tests/test_moderation_fixtures.py +113 -0
- xcore_protocol-0.5.1/python/tests/test_normalized_route_model.py +264 -0
- xcore_protocol-0.5.1/python/tests/test_telemetry_fixtures.py +49 -0
- xcore_protocol-0.5.1/python/xcore_protocol/__init__.py +1 -0
- xcore_protocol-0.5.1/python/xcore_protocol/generated/__init__.py +195 -0
- xcore_protocol-0.5.1/python/xcore_protocol/generated/chat.py +747 -0
- xcore_protocol-0.5.1/python/xcore_protocol/generated/discord.py +389 -0
- xcore_protocol-0.5.1/python/xcore_protocol/generated/envelopes.py +695 -0
- xcore_protocol-0.5.1/python/xcore_protocol/generated/maps.py +312 -0
- xcore_protocol-0.5.1/python/xcore_protocol/generated/moderation.py +478 -0
- xcore_protocol-0.5.1/python/xcore_protocol/generated/routes.py +634 -0
- xcore_protocol-0.5.1/python/xcore_protocol/generated/shared.py +605 -0
- xcore_protocol-0.5.1/python/xcore_protocol/generated/telemetry.py +177 -0
- xcore_protocol-0.5.1/python/xcore_protocol/models/__init__.py +1 -0
- xcore_protocol-0.5.1/python/xcore_protocol/paths.py +15 -0
- xcore_protocol-0.5.1/python/xcore_protocol/schema_validation.py +66 -0
- xcore_protocol-0.5.1/python/xcore_protocol.egg-info/PKG-INFO +92 -0
- xcore_protocol-0.5.1/python/xcore_protocol.egg-info/SOURCES.txt +36 -0
- xcore_protocol-0.5.1/python/xcore_protocol.egg-info/dependency_links.txt +1 -0
- xcore_protocol-0.5.1/python/xcore_protocol.egg-info/requires.txt +7 -0
- xcore_protocol-0.5.1/python/xcore_protocol.egg-info/top_level.txt +2 -0
- xcore_protocol-0.5.1/setup.cfg +4 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xcore-protocol
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: Canonical cross-service protocol for the XCore ecosystem
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: jsonschema>=4.23.0
|
|
8
|
+
Requires-Dist: rfc3339-validator>=0.1.4
|
|
9
|
+
Requires-Dist: referencing>=0.35.0
|
|
10
|
+
Requires-Dist: PyYAML>=6.0.2
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# xcore-protocol
|
|
15
|
+
|
|
16
|
+
Canonical cross-service protocol for the XCore ecosystem.
|
|
17
|
+
|
|
18
|
+
## Mission
|
|
19
|
+
`xcore-protocol` is the canonical source of truth for XCore cross-service communication.
|
|
20
|
+
|
|
21
|
+
It defines:
|
|
22
|
+
- message schemas
|
|
23
|
+
- envelope structure
|
|
24
|
+
- route and stream metadata
|
|
25
|
+
- versioning and compatibility rules
|
|
26
|
+
- canonical fixtures
|
|
27
|
+
- generation inputs and tooling
|
|
28
|
+
- generated Java and Python protocol DTO/model artifacts
|
|
29
|
+
- thin handwritten validation and repository-tooling support around generated artifacts
|
|
30
|
+
- cross-language compatibility tests
|
|
31
|
+
|
|
32
|
+
## Scope
|
|
33
|
+
This repository contains only **cross-process / cross-service wire protocol artifacts**.
|
|
34
|
+
|
|
35
|
+
### In scope
|
|
36
|
+
- event, command, and RPC contracts
|
|
37
|
+
- envelope definitions
|
|
38
|
+
- route metadata
|
|
39
|
+
- shared payload subtypes
|
|
40
|
+
- protocol fixtures and validation helpers
|
|
41
|
+
- generation configuration and generated Java/Python protocol artifacts
|
|
42
|
+
|
|
43
|
+
### Out of scope
|
|
44
|
+
- application business logic
|
|
45
|
+
- Discord handlers or presentation code
|
|
46
|
+
- Mindustry runtime integration
|
|
47
|
+
- Mongo persistence
|
|
48
|
+
- app-specific reconnect/worker orchestration
|
|
49
|
+
|
|
50
|
+
## Repository Layout
|
|
51
|
+
- `docs/` — ADRs, architecture notes, policies, migration notes
|
|
52
|
+
- `spec/` — protocol specs, shared types, envelopes, routes, generation inputs
|
|
53
|
+
- `fixtures/` — valid, invalid, and legacy examples
|
|
54
|
+
- `generators/` — language generation config/templates/scripts
|
|
55
|
+
- `java/` — generated Java protocol artifacts for the canonical model surface
|
|
56
|
+
- `python/` — generated Python protocol package and thin helpers
|
|
57
|
+
- `compat/` — cross-language compatibility checks
|
|
58
|
+
- `scripts/` — validation and generation helper scripts
|
|
59
|
+
|
|
60
|
+
## Migrated Families
|
|
61
|
+
|
|
62
|
+
All planned message families are fully migrated and operational:
|
|
63
|
+
|
|
64
|
+
- **Moderation** — ban, mute, vote-kick, kick-banned, pardon, audit
|
|
65
|
+
- **Discord** — link-confirm, unlink, link-status, admin-access, link-code
|
|
66
|
+
- **Maps** — list request/response, remove request/response
|
|
67
|
+
- **Chat/Heartbeat** — chat messages, global chat, heartbeat, join/leave, server actions, player state commands
|
|
68
|
+
- **Telemetry** — Redis TTL snapshot payloads for Prometheus/Gateway telemetry aggregation
|
|
69
|
+
|
|
70
|
+
## Current Status
|
|
71
|
+
|
|
72
|
+
All canonical message families are defined with JSON Schema specs and canonical fixtures. Generated Java records (`org.xcore.protocol.generated.*`) and Python frozen dataclasses (`xcore_protocol.generated.*`) cover the supported generated protocol surface, including route-less telemetry snapshot payloads. `XCore-plugin` and `XCore-discord-bot` both consume generated artifacts as their sole transport model where applicable. Cross-language compatibility checks and full CI validation chain are in place.
|
|
73
|
+
|
|
74
|
+
## Releases
|
|
75
|
+
|
|
76
|
+
Each `vX.Y.Z` tag publishes the same version of both supported artifacts:
|
|
77
|
+
|
|
78
|
+
- Java: `org.xcore:xcore-protocol-java:X.Y.Z` from `https://maven.x-core.org/releases`
|
|
79
|
+
- Python: `xcore-protocol==X.Y.Z` from PyPI
|
|
80
|
+
|
|
81
|
+
Production consumers must use an exact released version. Git revisions and `-SNAPSHOT`
|
|
82
|
+
versions are only acceptable while developing the protocol itself. The release workflow validates
|
|
83
|
+
schemas, generated output, Java tests, Python tests, and distributable packages before publishing.
|
|
84
|
+
|
|
85
|
+
Before the first PyPI release, configure the `pypi` GitHub environment as a PyPI Trusted Publisher
|
|
86
|
+
for this repository's `Publish Release` workflow. No Python package registry is self-hosted.
|
|
87
|
+
|
|
88
|
+
## See Also
|
|
89
|
+
- `docs/adr/ADR-001-protocol-first.md`
|
|
90
|
+
- `docs/architecture/protocol-overview.md`
|
|
91
|
+
- `docs/policies/versioning.md`
|
|
92
|
+
- `docs/policies/compatibility.md`
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# xcore-protocol
|
|
2
|
+
|
|
3
|
+
Canonical cross-service protocol for the XCore ecosystem.
|
|
4
|
+
|
|
5
|
+
## Mission
|
|
6
|
+
`xcore-protocol` is the canonical source of truth for XCore cross-service communication.
|
|
7
|
+
|
|
8
|
+
It defines:
|
|
9
|
+
- message schemas
|
|
10
|
+
- envelope structure
|
|
11
|
+
- route and stream metadata
|
|
12
|
+
- versioning and compatibility rules
|
|
13
|
+
- canonical fixtures
|
|
14
|
+
- generation inputs and tooling
|
|
15
|
+
- generated Java and Python protocol DTO/model artifacts
|
|
16
|
+
- thin handwritten validation and repository-tooling support around generated artifacts
|
|
17
|
+
- cross-language compatibility tests
|
|
18
|
+
|
|
19
|
+
## Scope
|
|
20
|
+
This repository contains only **cross-process / cross-service wire protocol artifacts**.
|
|
21
|
+
|
|
22
|
+
### In scope
|
|
23
|
+
- event, command, and RPC contracts
|
|
24
|
+
- envelope definitions
|
|
25
|
+
- route metadata
|
|
26
|
+
- shared payload subtypes
|
|
27
|
+
- protocol fixtures and validation helpers
|
|
28
|
+
- generation configuration and generated Java/Python protocol artifacts
|
|
29
|
+
|
|
30
|
+
### Out of scope
|
|
31
|
+
- application business logic
|
|
32
|
+
- Discord handlers or presentation code
|
|
33
|
+
- Mindustry runtime integration
|
|
34
|
+
- Mongo persistence
|
|
35
|
+
- app-specific reconnect/worker orchestration
|
|
36
|
+
|
|
37
|
+
## Repository Layout
|
|
38
|
+
- `docs/` — ADRs, architecture notes, policies, migration notes
|
|
39
|
+
- `spec/` — protocol specs, shared types, envelopes, routes, generation inputs
|
|
40
|
+
- `fixtures/` — valid, invalid, and legacy examples
|
|
41
|
+
- `generators/` — language generation config/templates/scripts
|
|
42
|
+
- `java/` — generated Java protocol artifacts for the canonical model surface
|
|
43
|
+
- `python/` — generated Python protocol package and thin helpers
|
|
44
|
+
- `compat/` — cross-language compatibility checks
|
|
45
|
+
- `scripts/` — validation and generation helper scripts
|
|
46
|
+
|
|
47
|
+
## Migrated Families
|
|
48
|
+
|
|
49
|
+
All planned message families are fully migrated and operational:
|
|
50
|
+
|
|
51
|
+
- **Moderation** — ban, mute, vote-kick, kick-banned, pardon, audit
|
|
52
|
+
- **Discord** — link-confirm, unlink, link-status, admin-access, link-code
|
|
53
|
+
- **Maps** — list request/response, remove request/response
|
|
54
|
+
- **Chat/Heartbeat** — chat messages, global chat, heartbeat, join/leave, server actions, player state commands
|
|
55
|
+
- **Telemetry** — Redis TTL snapshot payloads for Prometheus/Gateway telemetry aggregation
|
|
56
|
+
|
|
57
|
+
## Current Status
|
|
58
|
+
|
|
59
|
+
All canonical message families are defined with JSON Schema specs and canonical fixtures. Generated Java records (`org.xcore.protocol.generated.*`) and Python frozen dataclasses (`xcore_protocol.generated.*`) cover the supported generated protocol surface, including route-less telemetry snapshot payloads. `XCore-plugin` and `XCore-discord-bot` both consume generated artifacts as their sole transport model where applicable. Cross-language compatibility checks and full CI validation chain are in place.
|
|
60
|
+
|
|
61
|
+
## Releases
|
|
62
|
+
|
|
63
|
+
Each `vX.Y.Z` tag publishes the same version of both supported artifacts:
|
|
64
|
+
|
|
65
|
+
- Java: `org.xcore:xcore-protocol-java:X.Y.Z` from `https://maven.x-core.org/releases`
|
|
66
|
+
- Python: `xcore-protocol==X.Y.Z` from PyPI
|
|
67
|
+
|
|
68
|
+
Production consumers must use an exact released version. Git revisions and `-SNAPSHOT`
|
|
69
|
+
versions are only acceptable while developing the protocol itself. The release workflow validates
|
|
70
|
+
schemas, generated output, Java tests, Python tests, and distributable packages before publishing.
|
|
71
|
+
|
|
72
|
+
Before the first PyPI release, configure the `pypi` GitHub environment as a PyPI Trusted Publisher
|
|
73
|
+
for this repository's `Publish Release` workflow. No Python package registry is self-hosted.
|
|
74
|
+
|
|
75
|
+
## See Also
|
|
76
|
+
- `docs/adr/ADR-001-protocol-first.md`
|
|
77
|
+
- `docs/architecture/protocol-overview.md`
|
|
78
|
+
- `docs/policies/versioning.md`
|
|
79
|
+
- `docs/policies/compatibility.md`
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xcore-protocol"
|
|
7
|
+
version = "0.5.1"
|
|
8
|
+
description = "Canonical cross-service protocol for the XCore ecosystem"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"jsonschema>=4.23.0",
|
|
13
|
+
"rfc3339-validator>=0.1.4",
|
|
14
|
+
"referencing>=0.35.0",
|
|
15
|
+
"PyYAML>=6.0.2"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=8.3.0"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
testpaths = ["python/tests"]
|
|
25
|
+
pythonpath = ["python", "."]
|
|
26
|
+
addopts = "-q"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
package-dir = {"" = "python"}
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["python"]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from xcore_protocol.generated import (
|
|
11
|
+
ActorRefV1ActorType,
|
|
12
|
+
DiscordAdminAccessChangedCommandV1,
|
|
13
|
+
DiscordUnlinkCommandV1,
|
|
14
|
+
MetricsSnapshotV1,
|
|
15
|
+
ServerHeartbeatV1,
|
|
16
|
+
)
|
|
17
|
+
from xcore_protocol.paths import fixtures_root, spec_root
|
|
18
|
+
from xcore_protocol.schema_validation import assert_valid
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
REPO_ROOT = fixtures_root().parent
|
|
22
|
+
CANONICAL_FIXTURES_ROOT = REPO_ROOT / "spec" / "fixtures" / "valid"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_fixture(path: Path) -> dict[str, Any]:
|
|
26
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _model_dump_json(model: Any) -> dict[str, Any]:
|
|
30
|
+
model_dump = getattr(model, "model_dump", None)
|
|
31
|
+
if callable(model_dump):
|
|
32
|
+
payload = model_dump(mode="json")
|
|
33
|
+
if not isinstance(payload, dict):
|
|
34
|
+
raise TypeError("model_dump(mode='json') must return a dict payload")
|
|
35
|
+
return payload
|
|
36
|
+
|
|
37
|
+
to_payload = getattr(model, "to_payload", None)
|
|
38
|
+
if callable(to_payload):
|
|
39
|
+
payload = to_payload()
|
|
40
|
+
if not isinstance(payload, dict):
|
|
41
|
+
raise TypeError("to_payload() must return a dict payload")
|
|
42
|
+
return payload
|
|
43
|
+
|
|
44
|
+
raise TypeError("Generated model must provide model_dump(mode='json') or to_payload()")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _assert_no_snake_case_keys(value: Any) -> None:
|
|
48
|
+
if isinstance(value, Mapping):
|
|
49
|
+
for key, nested_value in value.items():
|
|
50
|
+
assert "_" not in key, f"Found legacy snake_case key: {key}"
|
|
51
|
+
_assert_no_snake_case_keys(nested_value)
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
if isinstance(value, list):
|
|
55
|
+
for item in value:
|
|
56
|
+
_assert_no_snake_case_keys(item)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
CanonicalAssertion = Callable[[Any, dict[str, Any]], None]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _assert_admin_access_semantics(model: DiscordAdminAccessChangedCommandV1, payload: dict[str, Any]) -> None:
|
|
63
|
+
assert model.source.actorType is ActorRefV1ActorType.SYSTEM
|
|
64
|
+
assert payload["source"]["actorType"] == "system"
|
|
65
|
+
assert model.actor.actorType is ActorRefV1ActorType.DISCORD
|
|
66
|
+
assert model.actor.actorName == payload["actor"]["actorName"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _assert_unlink_semantics(model: DiscordUnlinkCommandV1, payload: dict[str, Any]) -> None:
|
|
70
|
+
assert model.actor.actorType is ActorRefV1ActorType.DISCORD
|
|
71
|
+
assert model.actor.actorName == payload["actor"]["actorName"]
|
|
72
|
+
assert payload["actor"]["actorName"] == "Beta Display"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _assert_heartbeat_semantics(model: ServerHeartbeatV1, payload: dict[str, Any]) -> None:
|
|
76
|
+
assert model.serverName == payload["serverName"]
|
|
77
|
+
assert model.discordChannelId == payload["discordChannelId"]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _assert_telemetry_semantics(model: MetricsSnapshotV1, payload: dict[str, Any]) -> None:
|
|
81
|
+
assert model.server == payload["server"]
|
|
82
|
+
assert model.SCHEMA_VERSION == payload["schemaVersion"]
|
|
83
|
+
assert model.samples[2].count == payload["samples"][2]["count"]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
CANONICAL_CASES: list[tuple[str, Path, Path, type[Any], CanonicalAssertion]] = [
|
|
87
|
+
(
|
|
88
|
+
"discord_admin_access_grant",
|
|
89
|
+
spec_root() / "messages" / "discord" / "discord.admin-access.changed.command.v1.json",
|
|
90
|
+
CANONICAL_FIXTURES_ROOT / "discord" / "discord.admin-access.changed.command.v1.grant.json",
|
|
91
|
+
DiscordAdminAccessChangedCommandV1,
|
|
92
|
+
_assert_admin_access_semantics,
|
|
93
|
+
),
|
|
94
|
+
(
|
|
95
|
+
"discord_admin_access_revoke",
|
|
96
|
+
spec_root() / "messages" / "discord" / "discord.admin-access.changed.command.v1.json",
|
|
97
|
+
CANONICAL_FIXTURES_ROOT / "discord" / "discord.admin-access.changed.command.v1.revoke.json",
|
|
98
|
+
DiscordAdminAccessChangedCommandV1,
|
|
99
|
+
_assert_admin_access_semantics,
|
|
100
|
+
),
|
|
101
|
+
(
|
|
102
|
+
"discord_unlink",
|
|
103
|
+
spec_root() / "messages" / "discord" / "discord.unlink.command.v1.json",
|
|
104
|
+
CANONICAL_FIXTURES_ROOT / "discord" / "discord.unlink.command.v1.json",
|
|
105
|
+
DiscordUnlinkCommandV1,
|
|
106
|
+
_assert_unlink_semantics,
|
|
107
|
+
),
|
|
108
|
+
(
|
|
109
|
+
"server_heartbeat",
|
|
110
|
+
spec_root() / "messages" / "chat" / "server.heartbeat.v1.json",
|
|
111
|
+
CANONICAL_FIXTURES_ROOT / "chat" / "server.heartbeat.v1.json",
|
|
112
|
+
ServerHeartbeatV1,
|
|
113
|
+
_assert_heartbeat_semantics,
|
|
114
|
+
),
|
|
115
|
+
(
|
|
116
|
+
"metrics_snapshot",
|
|
117
|
+
spec_root() / "messages" / "telemetry" / "metrics.snapshot.v1.json",
|
|
118
|
+
CANONICAL_FIXTURES_ROOT / "telemetry" / "metrics.snapshot.v1.json",
|
|
119
|
+
MetricsSnapshotV1,
|
|
120
|
+
_assert_telemetry_semantics,
|
|
121
|
+
),
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_canonical_fixture_inventory_exists() -> None:
|
|
126
|
+
assert fixtures_root().exists()
|
|
127
|
+
|
|
128
|
+
for _, schema_path, fixture_path, _, _ in CANONICAL_CASES:
|
|
129
|
+
assert schema_path.exists()
|
|
130
|
+
assert fixture_path.exists()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.mark.parametrize(
|
|
134
|
+
("_case_name", "schema_path", "fixture_path", "model_type", "assert_semantics"),
|
|
135
|
+
CANONICAL_CASES,
|
|
136
|
+
)
|
|
137
|
+
def test_canonical_fixtures_remain_schema_valid(
|
|
138
|
+
_case_name: str,
|
|
139
|
+
schema_path: Path,
|
|
140
|
+
fixture_path: Path,
|
|
141
|
+
model_type: type[Any],
|
|
142
|
+
assert_semantics: CanonicalAssertion,
|
|
143
|
+
) -> None:
|
|
144
|
+
del model_type, assert_semantics
|
|
145
|
+
assert_valid(schema_path, fixture_path)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@pytest.mark.parametrize(
|
|
149
|
+
("_case_name", "schema_path", "fixture_path", "model_type", "assert_semantics"),
|
|
150
|
+
CANONICAL_CASES,
|
|
151
|
+
)
|
|
152
|
+
def test_canonical_generated_models_roundtrip(
|
|
153
|
+
_case_name: str,
|
|
154
|
+
schema_path: Path,
|
|
155
|
+
fixture_path: Path,
|
|
156
|
+
model_type: type[Any],
|
|
157
|
+
assert_semantics: CanonicalAssertion,
|
|
158
|
+
) -> None:
|
|
159
|
+
del schema_path
|
|
160
|
+
payload = _load_fixture(fixture_path)
|
|
161
|
+
|
|
162
|
+
model = model_type.from_payload(payload)
|
|
163
|
+
roundtrip_payload = _model_dump_json(model)
|
|
164
|
+
|
|
165
|
+
assert roundtrip_payload == payload
|
|
166
|
+
assert_semantics(model, payload)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_canonical_fixtures_do_not_use_legacy_snake_case_keys() -> None:
|
|
170
|
+
for _, _, fixture_path, _, _ in CANONICAL_CASES:
|
|
171
|
+
_assert_no_snake_case_keys(_load_fixture(fixture_path))
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from xcore_protocol.paths import fixtures_root, spec_root
|
|
4
|
+
from xcore_protocol.schema_validation import assert_invalid, assert_valid
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
VALID_CASES = [
|
|
8
|
+
(
|
|
9
|
+
spec_root() / "messages" / "chat" / "chat.message.v1.json",
|
|
10
|
+
fixtures_root() / "valid" / "chat" / "chat.message.v1.json",
|
|
11
|
+
),
|
|
12
|
+
(
|
|
13
|
+
spec_root() / "messages" / "chat" / "chat.global.v1.json",
|
|
14
|
+
fixtures_root() / "valid" / "chat" / "chat.global.v1.json",
|
|
15
|
+
),
|
|
16
|
+
(
|
|
17
|
+
spec_root() / "messages" / "chat" / "chat.discord-ingress.command.v1.json",
|
|
18
|
+
fixtures_root() / "valid" / "chat" / "chat.discord-ingress.command.v1.json",
|
|
19
|
+
),
|
|
20
|
+
(
|
|
21
|
+
spec_root() / "messages" / "chat" / "chat.private.v1.json",
|
|
22
|
+
fixtures_root() / "valid" / "chat" / "chat.private.v1.json",
|
|
23
|
+
),
|
|
24
|
+
(
|
|
25
|
+
spec_root() / "messages" / "chat" / "server.action.v1.json",
|
|
26
|
+
fixtures_root() / "valid" / "chat" / "server.action.v1.json",
|
|
27
|
+
),
|
|
28
|
+
(
|
|
29
|
+
spec_root() / "messages" / "chat" / "player.join-leave.v1.json",
|
|
30
|
+
fixtures_root() / "valid" / "chat" / "player.join-leave.v1.json",
|
|
31
|
+
),
|
|
32
|
+
(
|
|
33
|
+
spec_root() / "messages" / "chat" / "player.custom-nickname.changed.command.v1.json",
|
|
34
|
+
fixtures_root() / "valid" / "chat" / "player.custom-nickname.changed.command.v1.json",
|
|
35
|
+
),
|
|
36
|
+
(
|
|
37
|
+
spec_root() / "messages" / "chat" / "player.active-badge.changed.command.v1.json",
|
|
38
|
+
fixtures_root() / "valid" / "chat" / "player.active-badge.changed.command.v1.json",
|
|
39
|
+
),
|
|
40
|
+
(
|
|
41
|
+
spec_root() / "messages" / "chat" / "player.badge-inventory.changed.command.v1.json",
|
|
42
|
+
fixtures_root() / "valid" / "chat" / "player.badge-inventory.changed.command.v1.json",
|
|
43
|
+
),
|
|
44
|
+
(
|
|
45
|
+
spec_root() / "messages" / "chat" / "player.badge-symbol-color-mode.changed.command.v1.json",
|
|
46
|
+
fixtures_root() / "valid" / "chat" / "player.badge-symbol-color-mode.changed.command.v1.json",
|
|
47
|
+
),
|
|
48
|
+
(
|
|
49
|
+
spec_root() / "messages" / "chat" / "player.password-reset.command.v1.json",
|
|
50
|
+
fixtures_root() / "valid" / "chat" / "player.password-reset.command.v1.json",
|
|
51
|
+
),
|
|
52
|
+
(
|
|
53
|
+
spec_root() / "messages" / "chat" / "server.heartbeat.v1.json",
|
|
54
|
+
fixtures_root() / "valid" / "chat" / "server.heartbeat.v1.json",
|
|
55
|
+
),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
INVALID_CASES = [
|
|
60
|
+
(
|
|
61
|
+
spec_root() / "messages" / "chat" / "chat.message.v1.json",
|
|
62
|
+
fixtures_root() / "invalid" / "chat" / "chat.message.v1.snake-author.json",
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
spec_root() / "messages" / "chat" / "chat.private.v1.json",
|
|
66
|
+
fixtures_root() / "invalid" / "chat" / "chat.private.v1.snake-from.json",
|
|
67
|
+
),
|
|
68
|
+
(
|
|
69
|
+
spec_root() / "messages" / "chat" / "player.join-leave.v1.json",
|
|
70
|
+
fixtures_root()
|
|
71
|
+
/ "invalid"
|
|
72
|
+
/ "chat"
|
|
73
|
+
/ "player.join-leave.v1.legacy-join.json",
|
|
74
|
+
),
|
|
75
|
+
(
|
|
76
|
+
spec_root() / "messages" / "chat" / "player.custom-nickname.changed.command.v1.json",
|
|
77
|
+
fixtures_root()
|
|
78
|
+
/ "invalid"
|
|
79
|
+
/ "chat"
|
|
80
|
+
/ "player.custom-nickname.changed.command.v1.legacy-uuid.json",
|
|
81
|
+
),
|
|
82
|
+
(
|
|
83
|
+
spec_root() / "messages" / "chat" / "player.active-badge.changed.command.v1.json",
|
|
84
|
+
fixtures_root()
|
|
85
|
+
/ "invalid"
|
|
86
|
+
/ "chat"
|
|
87
|
+
/ "player.active-badge.changed.command.v1.legacy-uuid.json",
|
|
88
|
+
),
|
|
89
|
+
(
|
|
90
|
+
spec_root() / "messages" / "chat" / "player.badge-inventory.changed.command.v1.json",
|
|
91
|
+
fixtures_root()
|
|
92
|
+
/ "invalid"
|
|
93
|
+
/ "chat"
|
|
94
|
+
/ "player.badge-inventory.changed.command.v1.legacy-uuid.json",
|
|
95
|
+
),
|
|
96
|
+
(
|
|
97
|
+
spec_root() / "messages" / "chat" / "player.badge-symbol-color-mode.changed.command.v1.json",
|
|
98
|
+
fixtures_root()
|
|
99
|
+
/ "invalid"
|
|
100
|
+
/ "chat"
|
|
101
|
+
/ "player.badge-symbol-color-mode.changed.command.v1.legacy-uuid.json",
|
|
102
|
+
),
|
|
103
|
+
(
|
|
104
|
+
spec_root() / "messages" / "chat" / "player.password-reset.command.v1.json",
|
|
105
|
+
fixtures_root()
|
|
106
|
+
/ "invalid"
|
|
107
|
+
/ "chat"
|
|
108
|
+
/ "player.password-reset.command.v1.legacy-uuid.json",
|
|
109
|
+
),
|
|
110
|
+
(
|
|
111
|
+
spec_root() / "messages" / "chat" / "server.heartbeat.v1.json",
|
|
112
|
+
fixtures_root()
|
|
113
|
+
/ "invalid"
|
|
114
|
+
/ "chat"
|
|
115
|
+
/ "server.heartbeat.v1.server-host-alias.json",
|
|
116
|
+
),
|
|
117
|
+
(
|
|
118
|
+
spec_root() / "messages" / "chat" / "server.heartbeat.v1.json",
|
|
119
|
+
fixtures_root()
|
|
120
|
+
/ "invalid"
|
|
121
|
+
/ "chat"
|
|
122
|
+
/ "server.heartbeat.v1.missing-channel.json",
|
|
123
|
+
),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_valid_chat_fixtures_pass() -> None:
|
|
128
|
+
for schema_path, fixture_path in VALID_CASES:
|
|
129
|
+
assert_valid(schema_path, fixture_path)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_invalid_chat_fixtures_fail() -> None:
|
|
133
|
+
for schema_path, fixture_path in INVALID_CASES:
|
|
134
|
+
error = assert_invalid(schema_path, fixture_path)
|
|
135
|
+
assert error is not None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_chat_fixture_inventory_exists() -> None:
|
|
139
|
+
chat_valid_dir = fixtures_root() / "valid" / "chat"
|
|
140
|
+
chat_invalid_dir = fixtures_root() / "invalid" / "chat"
|
|
141
|
+
|
|
142
|
+
assert chat_valid_dir.exists()
|
|
143
|
+
assert chat_invalid_dir.exists()
|
|
144
|
+
assert any(chat_valid_dir.iterdir())
|
|
145
|
+
assert any(chat_invalid_dir.iterdir())
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from xcore_protocol.paths import fixtures_root, spec_root
|
|
4
|
+
from xcore_protocol.schema_validation import assert_invalid, assert_valid
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
VALID_CASES = [
|
|
8
|
+
(
|
|
9
|
+
spec_root() / "messages" / "discord" / "discord.link-code-created.v1.json",
|
|
10
|
+
fixtures_root() / "valid" / "discord" / "discord.link-code-created.v1.json",
|
|
11
|
+
),
|
|
12
|
+
(
|
|
13
|
+
spec_root() / "messages" / "discord" / "discord.link.confirm.command.v1.json",
|
|
14
|
+
fixtures_root() / "valid" / "discord" / "discord.link.confirm.command.v1.json",
|
|
15
|
+
),
|
|
16
|
+
(
|
|
17
|
+
spec_root() / "messages" / "discord" / "discord.unlink.command.v1.json",
|
|
18
|
+
fixtures_root() / "valid" / "discord" / "discord.unlink.command.v1.json",
|
|
19
|
+
),
|
|
20
|
+
(
|
|
21
|
+
spec_root() / "messages" / "discord" / "discord.link.status-changed.v1.json",
|
|
22
|
+
fixtures_root() / "valid" / "discord" / "discord.link.status-changed.v1.json",
|
|
23
|
+
),
|
|
24
|
+
(
|
|
25
|
+
spec_root()
|
|
26
|
+
/ "messages"
|
|
27
|
+
/ "discord"
|
|
28
|
+
/ "discord.admin-access.changed.command.v1.json",
|
|
29
|
+
fixtures_root()
|
|
30
|
+
/ "valid"
|
|
31
|
+
/ "discord"
|
|
32
|
+
/ "discord.admin-access.changed.command.v1.json",
|
|
33
|
+
),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
INVALID_CASES = [
|
|
38
|
+
(
|
|
39
|
+
spec_root() / "messages" / "discord" / "discord.link-code-created.v1.json",
|
|
40
|
+
fixtures_root() / "invalid" / "discord" / "discord.link-code-created.v1.bad-time.json",
|
|
41
|
+
),
|
|
42
|
+
(
|
|
43
|
+
spec_root() / "messages" / "discord" / "discord.link.confirm.command.v1.json",
|
|
44
|
+
fixtures_root()
|
|
45
|
+
/ "invalid"
|
|
46
|
+
/ "discord"
|
|
47
|
+
/ "discord.link.confirm.command.v1.bad-time.json",
|
|
48
|
+
),
|
|
49
|
+
(
|
|
50
|
+
spec_root() / "messages" / "discord" / "discord.unlink.command.v1.json",
|
|
51
|
+
fixtures_root()
|
|
52
|
+
/ "invalid"
|
|
53
|
+
/ "discord"
|
|
54
|
+
/ "discord.unlink.command.v1.missing-discord.json",
|
|
55
|
+
),
|
|
56
|
+
(
|
|
57
|
+
spec_root()
|
|
58
|
+
/ "messages"
|
|
59
|
+
/ "discord"
|
|
60
|
+
/ "discord.admin-access.changed.command.v1.json",
|
|
61
|
+
fixtures_root()
|
|
62
|
+
/ "invalid"
|
|
63
|
+
/ "discord"
|
|
64
|
+
/ "discord.admin-access.changed.command.v1.missing-reason.json",
|
|
65
|
+
),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_valid_discord_fixtures_pass() -> None:
|
|
70
|
+
for schema_path, fixture_path in VALID_CASES:
|
|
71
|
+
assert_valid(schema_path, fixture_path)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_invalid_discord_fixtures_fail() -> None:
|
|
75
|
+
for schema_path, fixture_path in INVALID_CASES:
|
|
76
|
+
error = assert_invalid(schema_path, fixture_path)
|
|
77
|
+
assert error is not None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_discord_fixture_inventory_exists() -> None:
|
|
81
|
+
discord_valid_dir = fixtures_root() / "valid" / "discord"
|
|
82
|
+
discord_invalid_dir = fixtures_root() / "invalid" / "discord"
|
|
83
|
+
|
|
84
|
+
assert discord_valid_dir.exists()
|
|
85
|
+
assert discord_invalid_dir.exists()
|
|
86
|
+
assert any(discord_valid_dir.iterdir())
|
|
87
|
+
assert any(discord_invalid_dir.iterdir())
|