diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d880c73 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + pull_request: + types: + - opened + - synchronize + - reopened + push: + branches: + - 'main' + tags: + - 'v*' + +# The only job here compiles schemas and validates XML; it needs nothing but read +# access to the checked-out tree. +permissions: + contents: read + +jobs: + schema: + name: Schema conformance + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + # No job pushes with git credentials; don't persist the token into + # .git/config where checked-out repo code could read it. + persist-credentials: false + - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 + with: + distribution: temurin + java-version: '17' + # Compiles every schema the SDKs generate their bindings from, then + # validates the captured payloads in test/fixtures against them. Fails on + # a schema that does not compile, a payload that does not validate, an + # attribute a payload carries but the schema does not declare, and a + # message type or endpoint with no payload at all. + - name: Check schemas against captured payloads + run: make check diff --git a/.gitignore b/.gitignore index a403a60..8f11836 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store /.idea/ +build/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ff338c0 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +BUILD_DIR := build + +.PHONY: check clean + +# Compile every schema and validate every captured payload against it. +check: $(BUILD_DIR)/SchemaCheck.class + @java -cp $(BUILD_DIR) SchemaCheck . + +$(BUILD_DIR)/SchemaCheck.class: tools/SchemaCheck.java + @mkdir -p $(BUILD_DIR) + @javac -d $(BUILD_DIR) tools/SchemaCheck.java + +clean: + @rm -rf $(BUILD_DIR) diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..323040e --- /dev/null +++ b/NOTES.md @@ -0,0 +1,106 @@ +Schema notes +===== + +Why this file exists: the schema, the producers and the SDK models had drifted +apart, and every reviewer who noticed re-derived the same conclusions from +scratch. This records the decisions so the next one does not have to. + +Conventions +----- + +**No XML namespace, anywhere.** The AMQP messages and the REST responses are both +served without a namespace, and none of the three SDKs looks for one. The REST +schemas used to declare `targetNamespace="http://schemas.oddin.gg/v1"` in 18 of +26 files, which meant no real response validated against them; the declaration +was removed rather than added to the remaining eight. + +**One definition per wire element.** `` and `` appear in +both the AMQP feed and the REST API and the producer emits the same shape in +both, so their attribute lists live once in `schema/common/` and are referenced +from `schema/feed/types.xsd` and `schema/rest/types/sport_event_status.xsd`. + +`` is deliberately still two types. It is not the same +element on the two wires: the feed sends `status` as a numeric enumeration plus +`match_status`, the REST API spells the status out (`live`, `closed`) and sends +`match_status_code`. Only its two child elements are shared. + +**Every added attribute is `use="optional"`,** because the producer emits a +sport-specific subset - a CS2 scoreboard carries rounds, a cricket scoreboard +carries overs and wickets. + +**Where the two copies of a shared element disagreed, the producer decided.** That +meant three type corrections: the per-period sub-scores are `xs:int` and not +`xs:decimal` (the producer sends integers and every SDK decodes integers), the +scores are `xs:double` and not `xs:decimal` (Go's float formatting can emit +exponent notation, which `xs:decimal` rejects), and `period_score/@number` is +required (the producer always sends it). + +**Every message type and endpoint has at least one captured payload** under +`test/fixtures/`, and `make check` validates them. No type declares +`xs:anyAttribute`, so an attribute a producer starts sending without declaring it +here fails the check. See [README.md](README.md#validating-the-schema). + +Attributes that exist in an SDK but on no wire +----- + +Legacy fields, carried over from the original code generation of the SDK models. +No Oddin producer emits them and nothing consumes them. + +**Verdict: do not add them to the schema.** They should be dropped from the SDK +models instead, one SDK at a time; that is out of scope here. The counts below +are the SDKs that still carry a field for the attribute, checked 2026-08-26. + +| Attribute | Where in the SDKs | Verdict | +| --- | --- | --- | +| `event_ref_id` | gosdk, netcoresdk, javasdk (as `ref_event_id`) | Legacy event alias. Not sent. | +| `ref_id` | all three, on nearly every element | Legacy id alias. Not sent. | +| `superceded_by` | javasdk; commented out in the producer as "not used" | Not sent. | +| `certainty` | javasdk | Not sent. | +| `betstop_reason`, `betting_status` | javasdk | Not sent. `bet_stop` carries `groups` and `market_status` instead - see `schema/feed/bet_stop.xsd`. | +| `odds_change_reason` | javasdk | Not sent. | +| `next_live_time`, `start_time_confirmed` | javasdk | Not sent. | +| `cashout_status`, `dead_heat_factor` | javasdk | Not sent. | +| `extended_specifiers` | gosdk, javasdk; commented out in the producer as "not used" | Not sent. `specifiers` is the only one. | +| `win_probabilities`, `lose_probabilities`, `half_win_probabilities`, `half_lose_probabilities`, `refund_probabilities` | javasdk | Not sent. The outcome carries a single `probabilities`. | +| `groups` on an odds_change market | javasdk | Not sent at market level. `groups` is real on `bet_stop` and on a REST `market_description`, which is what makes this one easy to misread. | +| `team` on an outcome | javasdk | Not sent. | +| `decided_by_fed`, `winning_reason`, `period` on the status | listed as SDK-only in the audit; not found in any SDK as of 2026-08-26 | Nothing to deprecate. | +| `void_reason_id`, `void_reason_params` on an odds_change or bet_settlement market | gosdk | Sent on `bet_cancel` markets only, where the schema declares them. | +| `match_status_code` on the **feed** `sport_event_status` | nowhere | The audit put it here by mistake. On the feed the attribute lives on `period_score` (declared, required) and on the SDK-only `results` element; the status itself sends `match_status`. Only `winner_id` was genuinely missing from the feed status. | + +Child elements in the same category: `results` (netcoresdk, javasdk as +`resultType`; a legacy per-period result list), `clock` (netcoresdk as +`clockType`, declared but never wired to a field), `statistics` and +`reference_ids` (gosdk), `market_metadata`, `odds_generation_properties` and +`delayed_info` (javasdk). None is emitted; none is declared. + +Questions the audit left open, now answered +----- + +* **`result` / `void_factor` on an odds_change outcome.** Not sent. gosdk reuses one + `Outcome` struct for `odds_change` and `bet_settlement`, which is where the + ambiguity came from; the producer sets them only when building a settlement. + They stay declared on the bet_settlement outcome only. +* **`icon_path` on `sport`.** Belongs to `sportExtended`, which only the `sports` + endpoint returns. The plain `sport` nested inside a tournament never carries + it. The schema was already right; gosdk merges the two types, which is what + made it look wrong. +* **`start_time` / `next_live_time` on `fixture_change`.** Not sent. The AMQP + `fixture_change` carries `change_type` and the standard message attributes, + nothing else. + +Known-latent, deliberately left alone +----- + +Places where the producer's model permits something the schema forbids, but which +has never been observed on the wire. Loosening them would turn a single object +into a list in every generated binding, so they are recorded rather than changed: + +* `teamExtended/sport` and `tournament/tournament_length` are slices in the + producer but `maxOccurs="1"` here. +* The producer can attach `` elements to `teamExtended` and + `tournament`. The schema declares no `category` element at all, and no + response has ever contained one. + +If any of these ever ships, `make check` will fail on the captured payload, which +is the point. diff --git a/README.md b/README.md index 6ef0878..84c626d 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,36 @@ Odds feed schema The Oddin odds feed is separated into two parts: a high performance messaging feed through AMQP (Advanced Message Queuing Protocol) and a classic REST based XML API. + +### Layout + +``` +schema/feed/ one file per AMQP message, plus the shared types.xsd +schema/rest/ one file per REST endpoint response +schema/common/ attribute groups for the elements both wires share +test/fixtures/ captured payloads, one directory per schema +``` + +Neither wire uses an XML namespace, so no schema declares a `targetNamespace`. + +### Validating the schema + +`make check` compiles every schema and validates every captured payload in +`test/fixtures///` against +`schema//.xsd`. It needs a JDK and nothing else, and it +is what CI runs on every pull request. + +It fails when + +* a schema does not compile - the SDKs generate their bindings from these files, + so a schema that does not compile is a schema nobody can consume; +* a captured payload does not validate; +* a payload carries an attribute the schema does not declare (no type here + declares `xs:anyAttribute`, so this is how silent producer drift is caught); +* a message type or endpoint has no captured payload at all. + +When a producer starts sending something new, add the attribute to the schema and +a payload that exercises it in the same change. + +[NOTES.md](NOTES.md) records the conventions, which attributes exist in an SDK but +on no wire, and why `` is defined twice on purpose. diff --git a/schema/common/period_score.xsd b/schema/common/period_score.xsd new file mode 100644 index 0000000..02c8e9a --- /dev/null +++ b/schema/common/period_score.xsd @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/schema/common/scoreboard.xsd b/schema/common/scoreboard.xsd new file mode 100644 index 0000000..7eb2b53 --- /dev/null +++ b/schema/common/scoreboard.xsd @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/schema/feed/bet_stop.xsd b/schema/feed/bet_stop.xsd new file mode 100644 index 0000000..8455b41 --- /dev/null +++ b/schema/feed/bet_stop.xsd @@ -0,0 +1,18 @@ + + + + + + + + + + Pipe separated list of the market groups the stop applies + to, or "all" for every market of the event. + + + + + + + diff --git a/schema/feed/types.xsd b/schema/feed/types.xsd index f2959fb..5d4b984 100644 --- a/schema/feed/types.xsd +++ b/schema/feed/types.xsd @@ -1,5 +1,8 @@ + + + @@ -80,32 +83,14 @@ - - + + + - - - - - - - - - - - - - - - - - - - - + @@ -115,19 +100,7 @@ - - - - - - - - - - - - - + diff --git a/schema/rest/competitor.xsd b/schema/rest/competitor.xsd index 4a23d0f..495c7d4 100644 --- a/schema/rest/competitor.xsd +++ b/schema/rest/competitor.xsd @@ -1,5 +1,5 @@ - + @@ -11,7 +11,7 @@ - + diff --git a/schema/rest/error.xsd b/schema/rest/error.xsd new file mode 100644 index 0000000..ed16874 --- /dev/null +++ b/schema/rest/error.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/schema/rest/fixture_changes.xsd b/schema/rest/fixture_changes.xsd index 7fdba66..d3118df 100644 --- a/schema/rest/fixture_changes.xsd +++ b/schema/rest/fixture_changes.xsd @@ -1,5 +1,5 @@ - + @@ -11,7 +11,7 @@ - + diff --git a/schema/rest/fixtures_fixture.xsd b/schema/rest/fixtures_fixture.xsd index c2c9c85..00352f9 100644 --- a/schema/rest/fixtures_fixture.xsd +++ b/schema/rest/fixtures_fixture.xsd @@ -1,5 +1,5 @@ - + @@ -30,11 +30,6 @@ - - - - - @@ -44,6 +39,7 @@ + diff --git a/schema/rest/markets.xsd b/schema/rest/markets.xsd index 423c349..68822fd 100644 --- a/schema/rest/markets.xsd +++ b/schema/rest/markets.xsd @@ -1,5 +1,5 @@ - + diff --git a/schema/rest/match_status.xsd b/schema/rest/match_status.xsd index 63a7553..4d1e995 100644 --- a/schema/rest/match_status.xsd +++ b/schema/rest/match_status.xsd @@ -1,5 +1,5 @@ - + diff --git a/schema/rest/match_summary.xsd b/schema/rest/match_summary.xsd index 70a0827..9f53278 100644 --- a/schema/rest/match_summary.xsd +++ b/schema/rest/match_summary.xsd @@ -1,5 +1,5 @@ - + diff --git a/schema/rest/player.xsd b/schema/rest/player.xsd index 956a0e6..fa267d8 100644 --- a/schema/rest/player.xsd +++ b/schema/rest/player.xsd @@ -1,5 +1,5 @@ - + @@ -7,5 +7,6 @@ + diff --git a/schema/rest/producers.xsd b/schema/rest/producers.xsd index 4d5162e..b2cfab7 100644 --- a/schema/rest/producers.xsd +++ b/schema/rest/producers.xsd @@ -1,5 +1,5 @@ - + diff --git a/schema/rest/replay_content.xsd b/schema/rest/replay_content.xsd index 4b9aeda..c6fb0a4 100644 --- a/schema/rest/replay_content.xsd +++ b/schema/rest/replay_content.xsd @@ -1,5 +1,5 @@ - + diff --git a/schema/rest/replay_status.xsd b/schema/rest/replay_status.xsd index f1e3b9c..6bd328f 100644 --- a/schema/rest/replay_status.xsd +++ b/schema/rest/replay_status.xsd @@ -1,5 +1,5 @@ - + diff --git a/schema/rest/schedule.xsd b/schema/rest/schedule.xsd index 4b2ed51..1d44ade 100644 --- a/schema/rest/schedule.xsd +++ b/schema/rest/schedule.xsd @@ -1,5 +1,5 @@ - + diff --git a/schema/rest/sport_tournaments.xsd b/schema/rest/sport_tournaments.xsd index d82ce42..76ecd82 100644 --- a/schema/rest/sport_tournaments.xsd +++ b/schema/rest/sport_tournaments.xsd @@ -1,5 +1,5 @@ - + diff --git a/schema/rest/sports.xsd b/schema/rest/sports.xsd index 5229596..e3429fd 100644 --- a/schema/rest/sports.xsd +++ b/schema/rest/sports.xsd @@ -1,5 +1,5 @@ - + diff --git a/schema/rest/tournament_info.xsd b/schema/rest/tournament_info.xsd index 5afe33f..8ad1330 100644 --- a/schema/rest/tournament_info.xsd +++ b/schema/rest/tournament_info.xsd @@ -1,5 +1,5 @@ - + diff --git a/schema/rest/tournament_schedule.xsd b/schema/rest/tournament_schedule.xsd index 6d7fbbf..a137328 100644 --- a/schema/rest/tournament_schedule.xsd +++ b/schema/rest/tournament_schedule.xsd @@ -1,5 +1,5 @@ - + @@ -14,7 +14,7 @@ - + diff --git a/schema/rest/tournaments.xsd b/schema/rest/tournaments.xsd index 0fa8509..5cac84d 100644 --- a/schema/rest/tournaments.xsd +++ b/schema/rest/tournaments.xsd @@ -1,5 +1,5 @@ - + diff --git a/schema/rest/types/sport.xsd b/schema/rest/types/sport.xsd index 5364384..7d09ca6 100644 --- a/schema/rest/types/sport.xsd +++ b/schema/rest/types/sport.xsd @@ -3,7 +3,7 @@ - + diff --git a/schema/rest/types/sport_event.xsd b/schema/rest/types/sport_event.xsd index 494cd7f..a18feba 100644 --- a/schema/rest/types/sport_event.xsd +++ b/schema/rest/types/sport_event.xsd @@ -27,9 +27,17 @@ + + + + + + + + diff --git a/schema/rest/types/sport_event_status.xsd b/schema/rest/types/sport_event_status.xsd index 5786a6c..1e57e11 100644 --- a/schema/rest/types/sport_event_status.xsd +++ b/schema/rest/types/sport_event_status.xsd @@ -1,32 +1,37 @@ - + + + - + + - + - - + + - - - - - - - - - - - - - - + + + + + @@ -35,49 +40,11 @@ - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - + diff --git a/schema/rest/types/team.xsd b/schema/rest/types/team.xsd index da729be..4977ba8 100644 --- a/schema/rest/types/team.xsd +++ b/schema/rest/types/team.xsd @@ -8,6 +8,9 @@ + + + diff --git a/schema/rest/types/urn.xsd b/schema/rest/types/urn.xsd index b0a34fb..cb456e0 100644 --- a/schema/rest/types/urn.xsd +++ b/schema/rest/types/urn.xsd @@ -35,4 +35,10 @@ + + + + + + diff --git a/schema/rest/void_reasons.xsd b/schema/rest/void_reasons.xsd index 63f08b9..64927c4 100644 --- a/schema/rest/void_reasons.xsd +++ b/schema/rest/void_reasons.xsd @@ -8,7 +8,7 @@ - + diff --git a/schema/rest/whoami.xsd b/schema/rest/whoami.xsd index 59ab837..51a1bb8 100644 --- a/schema/rest/whoami.xsd +++ b/schema/rest/whoami.xsd @@ -1,5 +1,5 @@ - + diff --git a/test/fixtures/feed/alive/alive.xml b/test/fixtures/feed/alive/alive.xml new file mode 100644 index 0000000..692d7c6 --- /dev/null +++ b/test/fixtures/feed/alive/alive.xml @@ -0,0 +1 @@ + diff --git a/test/fixtures/feed/bet_cancel/bet_cancel.xml b/test/fixtures/feed/bet_cancel/bet_cancel.xml new file mode 100644 index 0000000..5f2be16 --- /dev/null +++ b/test/fixtures/feed/bet_cancel/bet_cancel.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/test/fixtures/feed/bet_settlement/bet_settlement.xml b/test/fixtures/feed/bet_settlement/bet_settlement.xml new file mode 100644 index 0000000..d512b4c --- /dev/null +++ b/test/fixtures/feed/bet_settlement/bet_settlement.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/fixtures/feed/bet_stop/bet_stop_all_groups.xml b/test/fixtures/feed/bet_stop/bet_stop_all_groups.xml new file mode 100644 index 0000000..dad2b22 --- /dev/null +++ b/test/fixtures/feed/bet_stop/bet_stop_all_groups.xml @@ -0,0 +1 @@ + diff --git a/test/fixtures/feed/bet_stop/bet_stop_minimal.xml b/test/fixtures/feed/bet_stop/bet_stop_minimal.xml new file mode 100644 index 0000000..c7b87e0 --- /dev/null +++ b/test/fixtures/feed/bet_stop/bet_stop_minimal.xml @@ -0,0 +1 @@ + diff --git a/test/fixtures/feed/fixture_change/fixture_change.xml b/test/fixtures/feed/fixture_change/fixture_change.xml new file mode 100644 index 0000000..466e706 --- /dev/null +++ b/test/fixtures/feed/fixture_change/fixture_change.xml @@ -0,0 +1 @@ + diff --git a/test/fixtures/feed/odds_change/odds_change_closed_with_winner.xml b/test/fixtures/feed/odds_change/odds_change_closed_with_winner.xml new file mode 100644 index 0000000..8dda2a4 --- /dev/null +++ b/test/fixtures/feed/odds_change/odds_change_closed_with_winner.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/test/fixtures/feed/odds_change/odds_change_cricket_scoreboard.xml b/test/fixtures/feed/odds_change/odds_change_cricket_scoreboard.xml new file mode 100644 index 0000000..2108720 --- /dev/null +++ b/test/fixtures/feed/odds_change/odds_change_cricket_scoreboard.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/test/fixtures/feed/odds_change/odds_change_markets_only.xml b/test/fixtures/feed/odds_change/odds_change_markets_only.xml new file mode 100644 index 0000000..c657e32 --- /dev/null +++ b/test/fixtures/feed/odds_change/odds_change_markets_only.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/test/fixtures/feed/odds_change/odds_change_moba_scoreboard.xml b/test/fixtures/feed/odds_change/odds_change_moba_scoreboard.xml new file mode 100644 index 0000000..ca54422 --- /dev/null +++ b/test/fixtures/feed/odds_change/odds_change_moba_scoreboard.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/test/fixtures/feed/odds_change/odds_change_points_scoreboard.xml b/test/fixtures/feed/odds_change/odds_change_points_scoreboard.xml new file mode 100644 index 0000000..58d469b --- /dev/null +++ b/test/fixtures/feed/odds_change/odds_change_points_scoreboard.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/test/fixtures/feed/odds_change/odds_change_rounds_scoreboard.xml b/test/fixtures/feed/odds_change/odds_change_rounds_scoreboard.xml new file mode 100644 index 0000000..e3d131c --- /dev/null +++ b/test/fixtures/feed/odds_change/odds_change_rounds_scoreboard.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/test/fixtures/feed/odds_change/odds_change_set_based_classic_sport.xml b/test/fixtures/feed/odds_change/odds_change_set_based_classic_sport.xml new file mode 100644 index 0000000..dc21271 --- /dev/null +++ b/test/fixtures/feed/odds_change/odds_change_set_based_classic_sport.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/test/fixtures/feed/odds_change/odds_change_soccer_scoreboard.xml b/test/fixtures/feed/odds_change/odds_change_soccer_scoreboard.xml new file mode 100644 index 0000000..d45a3c9 --- /dev/null +++ b/test/fixtures/feed/odds_change/odds_change_soccer_scoreboard.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/test/fixtures/feed/odds_change/odds_change_table_tennis_scoreboard.xml b/test/fixtures/feed/odds_change/odds_change_table_tennis_scoreboard.xml new file mode 100644 index 0000000..4f26a9e --- /dev/null +++ b/test/fixtures/feed/odds_change/odds_change_table_tennis_scoreboard.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/fixtures/feed/rollback_bet_cancel/rollback_bet_cancel.xml b/test/fixtures/feed/rollback_bet_cancel/rollback_bet_cancel.xml new file mode 100644 index 0000000..61d670b --- /dev/null +++ b/test/fixtures/feed/rollback_bet_cancel/rollback_bet_cancel.xml @@ -0,0 +1,3 @@ + + + diff --git a/test/fixtures/feed/rollback_bet_settlement/rollback_bet_settlement.xml b/test/fixtures/feed/rollback_bet_settlement/rollback_bet_settlement.xml new file mode 100644 index 0000000..7403bd1 --- /dev/null +++ b/test/fixtures/feed/rollback_bet_settlement/rollback_bet_settlement.xml @@ -0,0 +1,4 @@ + + + + diff --git a/test/fixtures/feed/snapshot_complete/snapshot_complete.xml b/test/fixtures/feed/snapshot_complete/snapshot_complete.xml new file mode 100644 index 0000000..dda3551 --- /dev/null +++ b/test/fixtures/feed/snapshot_complete/snapshot_complete.xml @@ -0,0 +1 @@ + diff --git a/test/fixtures/rest/competitor/competitor_profile.xml b/test/fixtures/rest/competitor/competitor_profile.xml new file mode 100644 index 0000000..5934f1b --- /dev/null +++ b/test/fixtures/rest/competitor/competitor_profile.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/test/fixtures/rest/competitor/competitor_profile_no_players.xml b/test/fixtures/rest/competitor/competitor_profile_no_players.xml new file mode 100644 index 0000000..9351627 --- /dev/null +++ b/test/fixtures/rest/competitor/competitor_profile_no_players.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/fixtures/rest/error/not_found.xml b/test/fixtures/rest/error/not_found.xml new file mode 100644 index 0000000..1b1dfea --- /dev/null +++ b/test/fixtures/rest/error/not_found.xml @@ -0,0 +1,5 @@ + + + Invalid market ID.. Not Found + ERROR. Invalid market ID.. Not Found + diff --git a/test/fixtures/rest/fixture_changes/fixture_changes.xml b/test/fixtures/rest/fixture_changes/fixture_changes.xml new file mode 100644 index 0000000..913cac2 --- /dev/null +++ b/test/fixtures/rest/fixture_changes/fixture_changes.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/test/fixtures/rest/fixtures_fixture/fixtures_fixture.xml b/test/fixtures/rest/fixtures_fixture/fixtures_fixture.xml new file mode 100644 index 0000000..df79db5 --- /dev/null +++ b/test/fixtures/rest/fixtures_fixture/fixtures_fixture.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/rest/markets/market_descriptions.xml b/test/fixtures/rest/markets/market_descriptions.xml new file mode 100644 index 0000000..c7a7c6f --- /dev/null +++ b/test/fixtures/rest/markets/market_descriptions.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/rest/markets/market_descriptions_empty.xml b/test/fixtures/rest/markets/market_descriptions_empty.xml new file mode 100644 index 0000000..d295bec --- /dev/null +++ b/test/fixtures/rest/markets/market_descriptions_empty.xml @@ -0,0 +1,2 @@ + + diff --git a/test/fixtures/rest/match_status/match_status_descriptions.xml b/test/fixtures/rest/match_status/match_status_descriptions.xml new file mode 100644 index 0000000..c9a5d6c --- /dev/null +++ b/test/fixtures/rest/match_status/match_status_descriptions.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/test/fixtures/rest/match_summary/match_summary.xml b/test/fixtures/rest/match_summary/match_summary.xml new file mode 100644 index 0000000..8d6aaf2 --- /dev/null +++ b/test/fixtures/rest/match_summary/match_summary.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/rest/match_summary/match_summary_cricket_scoreboard.xml b/test/fixtures/rest/match_summary/match_summary_cricket_scoreboard.xml new file mode 100644 index 0000000..98eb19a --- /dev/null +++ b/test/fixtures/rest/match_summary/match_summary_cricket_scoreboard.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/test/fixtures/rest/player/player_profile.xml b/test/fixtures/rest/player/player_profile.xml new file mode 100644 index 0000000..92be634 --- /dev/null +++ b/test/fixtures/rest/player/player_profile.xml @@ -0,0 +1,4 @@ + + + + diff --git a/test/fixtures/rest/producers/producers.xml b/test/fixtures/rest/producers/producers.xml new file mode 100644 index 0000000..0bbc984 --- /dev/null +++ b/test/fixtures/rest/producers/producers.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/test/fixtures/rest/replay_content/replay_set_content.xml b/test/fixtures/rest/replay_content/replay_set_content.xml new file mode 100644 index 0000000..3438eb4 --- /dev/null +++ b/test/fixtures/rest/replay_content/replay_set_content.xml @@ -0,0 +1,4 @@ + + + + diff --git a/test/fixtures/rest/replay_status/player_status.xml b/test/fixtures/rest/replay_status/player_status.xml new file mode 100644 index 0000000..ef9db2f --- /dev/null +++ b/test/fixtures/rest/replay_status/player_status.xml @@ -0,0 +1,2 @@ + + diff --git a/test/fixtures/rest/schedule/schedule.xml b/test/fixtures/rest/schedule/schedule.xml new file mode 100644 index 0000000..c745a04 --- /dev/null +++ b/test/fixtures/rest/schedule/schedule.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/rest/sport_tournaments/sport_tournaments.xml b/test/fixtures/rest/sport_tournaments/sport_tournaments.xml new file mode 100644 index 0000000..9fa11b1 --- /dev/null +++ b/test/fixtures/rest/sport_tournaments/sport_tournaments.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/test/fixtures/rest/sports/sports.xml b/test/fixtures/rest/sports/sports.xml new file mode 100644 index 0000000..8067226 --- /dev/null +++ b/test/fixtures/rest/sports/sports.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/test/fixtures/rest/tournament_info/tournament_info.xml b/test/fixtures/rest/tournament_info/tournament_info.xml new file mode 100644 index 0000000..ff9632e --- /dev/null +++ b/test/fixtures/rest/tournament_info/tournament_info.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/test/fixtures/rest/tournament_schedule/tournament_schedule.xml b/test/fixtures/rest/tournament_schedule/tournament_schedule.xml new file mode 100644 index 0000000..61d41c0 --- /dev/null +++ b/test/fixtures/rest/tournament_schedule/tournament_schedule.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/test/fixtures/rest/tournaments/tournaments.xml b/test/fixtures/rest/tournaments/tournaments.xml new file mode 100644 index 0000000..f0602a3 --- /dev/null +++ b/test/fixtures/rest/tournaments/tournaments.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/test/fixtures/rest/tournaments/tournaments_empty.xml b/test/fixtures/rest/tournaments/tournaments_empty.xml new file mode 100644 index 0000000..ccda6ec --- /dev/null +++ b/test/fixtures/rest/tournaments/tournaments_empty.xml @@ -0,0 +1,2 @@ + + diff --git a/test/fixtures/rest/void_reasons/void_reasons.xml b/test/fixtures/rest/void_reasons/void_reasons.xml new file mode 100644 index 0000000..a7537cd --- /dev/null +++ b/test/fixtures/rest/void_reasons/void_reasons.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/fixtures/rest/whoami/bookmaker_details.xml b/test/fixtures/rest/whoami/bookmaker_details.xml new file mode 100644 index 0000000..1e3d718 --- /dev/null +++ b/test/fixtures/rest/whoami/bookmaker_details.xml @@ -0,0 +1,2 @@ + + diff --git a/tools/SchemaCheck.java b/tools/SchemaCheck.java new file mode 100644 index 0000000..bbc7264 --- /dev/null +++ b/tools/SchemaCheck.java @@ -0,0 +1,224 @@ +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXParseException; + +/** + * Conformance gate for the odds feed schema. + * + *

Two checks, both of which have to hold for the repository to describe the + * live contract: + * + *

    + *
  1. every top level schema under schema/feed and schema/rest compiles - the + * SDKs generate their bindings from these files, so a schema that does not + * compile is a schema nobody can consume; + *
  2. every captured payload under test/fixtures/{feed,rest}/<name>/*.xml + * validates against schema/{feed,rest}/<name>.xsd. No type in the + * schema declares xs:anyAttribute, so an attribute a producer starts + * sending without declaring it here fails this step. + *
+ * + *

Usage: {@code java SchemaCheck } + */ +public final class SchemaCheck { + + private static final String XSD_NS = XMLConstants.W3C_XML_SCHEMA_NS_URI; + + /** Directories holding schema files, each mirrored by test/fixtures/<section>. */ + private static final String[] SECTIONS = {"common", "feed", "rest"}; + + public static void main(String[] args) throws Exception { + Path root = Paths.get(args.length > 0 ? args[0] : ".").toAbsolutePath().normalize(); + + List failures = new ArrayList<>(); + int schemas = 0; + int fixtures = 0; + + for (String section : SECTIONS) { + for (Path xsd : listXsds(root.resolve("schema").resolve(section))) { + schemas++; + String name = fileName(xsd).replaceFirst("\\.xsd$", ""); + + Schema schema; + try { + schema = compile(xsd); + } catch (SAXParseException e) { + failures.add(rel(root, xsd) + ": does not compile: " + describe(e)); + continue; + } + System.out.println("schema ok " + rel(root, xsd)); + + // A schema with no global element is a shared type library + // (schema/common, schema/feed/types.xsd): nothing can be an + // instance of it, so there is nothing to validate against it. + if (!declaresGlobalElement(xsd)) { + continue; + } + + Path fixtureDir = root.resolve("test/fixtures").resolve(section).resolve(name); + List payloads = listXmls(fixtureDir); + if (payloads.isEmpty()) { + failures.add(rel(root, xsd) + ": no payload in " + rel(root, fixtureDir) + + " - every message type and endpoint needs at least one"); + continue; + } + for (Path payload : payloads) { + fixtures++; + List errors = validate(schema, payload); + if (errors.isEmpty()) { + System.out.println("payload ok " + rel(root, payload)); + } else { + for (String error : errors) { + failures.add(rel(root, payload) + ": " + error); + } + } + } + } + } + + // Payloads whose schema was renamed or removed would otherwise never run. + for (String section : SECTIONS) { + Path fixtureRoot = root.resolve("test/fixtures").resolve(section); + if (!Files.isDirectory(fixtureRoot)) { + continue; + } + try (Stream dirs = Files.list(fixtureRoot)) { + for (Path dir : dirs.filter(Files::isDirectory).sorted().toArray(Path[]::new)) { + Path xsd = root.resolve("schema").resolve(section) + .resolve(fileName(dir) + ".xsd"); + if (!Files.isRegularFile(xsd)) { + failures.add(rel(root, dir) + ": no matching " + rel(root, xsd)); + } + } + } + } + + System.out.println(); + System.out.printf("%d schemas, %d payloads, %d problems%n", + schemas, fixtures, failures.size()); + if (!failures.isEmpty()) { + System.out.println(); + for (String failure : failures) { + System.out.println("FAIL " + failure); + } + System.exit(1); + } + } + + private static boolean declaresGlobalElement(Path xsd) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + Document document = factory.newDocumentBuilder().parse(xsd.toFile()); + NodeList children = document.getDocumentElement().getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE + && XSD_NS.equals(child.getNamespaceURI()) + && "element".equals(child.getLocalName())) { + return true; + } + } + return false; + } + + private static Schema compile(Path xsd) throws Exception { + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + factory.setErrorHandler(new FailFast()); + return factory.newSchema(new StreamSource(xsd.toFile())); + } + + private static List validate(Schema schema, Path payload) throws Exception { + Collect collect = new Collect(); + Validator validator = schema.newValidator(); + validator.setErrorHandler(collect); + try { + validator.validate(new StreamSource(payload.toFile())); + } catch (SAXParseException e) { + collect.messages.add(describe(e)); + } + return collect.messages; + } + + private static List listXsds(Path dir) throws IOException { + return list(dir, ".xsd"); + } + + private static List listXmls(Path dir) throws IOException { + return list(dir, ".xml"); + } + + private static List list(Path dir, String suffix) throws IOException { + if (!Files.isDirectory(dir)) { + return new ArrayList<>(); + } + try (Stream entries = Files.list(dir)) { + List found = new ArrayList<>(); + entries.filter(Files::isRegularFile) + .filter(p -> fileName(p).endsWith(suffix)) + .sorted(Comparator.comparing(SchemaCheck::fileName)) + .forEach(found::add); + return found; + } + } + + private static String describe(SAXParseException e) { + return "line " + e.getLineNumber() + ": " + e.getMessage(); + } + + private static String fileName(Path path) { + return path.getFileName().toString(); + } + + private static String rel(Path root, Path path) { + return root.relativize(path).toString(); + } + + /** Turns schema compilation warnings into failures too - a warning here is drift. */ + private static final class FailFast implements ErrorHandler { + public void warning(SAXParseException e) throws SAXParseException { + throw e; + } + + public void error(SAXParseException e) throws SAXParseException { + throw e; + } + + public void fatalError(SAXParseException e) throws SAXParseException { + throw e; + } + } + + /** Reports every problem in a payload rather than stopping at the first. */ + private static final class Collect implements ErrorHandler { + private final List messages = new ArrayList<>(); + + public void warning(SAXParseException e) { + messages.add(describe(e)); + } + + public void error(SAXParseException e) { + messages.add(describe(e)); + } + + public void fatalError(SAXParseException e) { + messages.add(describe(e)); + } + } +}