Add sync schema contract guardrails

This commit is contained in:
Daniel Schädler 2026-04-16 10:23:52 +02:00
parent f18c37401e
commit 5126e94c26
3 changed files with 107 additions and 56 deletions

View File

@ -1,6 +1,14 @@
# frozen_string_literal: true
class TournamentSnapshotBuilder
ROOT_ATTRIBUTES = TournamentSyncSchema.synced_columns_for(Tournament).map(&:to_sym).freeze
TEAM_ATTRIBUTES = TournamentSyncSchema.synced_columns_for(Team).map(&:to_sym).freeze
STAGE_ATTRIBUTES = TournamentSyncSchema.synced_columns_for(Stage).map(&:to_sym).freeze
GROUP_ATTRIBUTES = TournamentSyncSchema.synced_columns_for(Group).map(&:to_sym).freeze
MATCH_ATTRIBUTES = TournamentSyncSchema.synced_columns_for(Match).map(&:to_sym).freeze
MATCH_SCORE_ATTRIBUTES = TournamentSyncSchema.synced_columns_for(MatchScore).map(&:to_sym).freeze
GROUP_SCORE_ATTRIBUTES = TournamentSyncSchema.synced_columns_for(GroupScore).map(&:to_sym).freeze
def self.build(tournament)
new(tournament).build
end
@ -10,24 +18,16 @@ class TournamentSnapshotBuilder
end
def build
{
id: tournament.id,
name: tournament.name,
code: tournament.code,
description: tournament.description,
public: tournament.public,
playoff_teams_amount: tournament.playoff_teams_amount,
instant_finalists_amount: tournament.instant_finalists_amount,
intermediate_round_participants_amount: tournament.intermediate_round_participants_amount,
sync_generated_at: Time.current.iso8601(6),
timer_end: tournament.timer_end&.iso8601,
teams: teams_payload,
stages: stages_payload,
groups: groups_payload,
matches: matches_payload,
match_scores: match_scores_payload,
group_scores: group_scores_payload
}
payload = tournament.attributes.symbolize_keys.slice(*ROOT_ATTRIBUTES)
payload[:timer_end] = tournament.timer_end&.iso8601
payload[:sync_generated_at] = Time.current.iso8601(6)
payload[:teams] = teams_payload
payload[:stages] = stages_payload
payload[:groups] = groups_payload
payload[:matches] = matches_payload
payload[:match_scores] = match_scores_payload
payload[:group_scores] = group_scores_payload
payload
end
private
@ -36,69 +36,37 @@ class TournamentSnapshotBuilder
def teams_payload
teams_for_snapshot.sort_by(&:id).map do |team|
{
id: team.id,
name: team.name
}
team.attributes.symbolize_keys.slice(*TEAM_ATTRIBUTES)
end
end
def stages_payload
tournament.stages.order(:id).map do |stage|
{
id: stage.id,
level: stage.level,
state: stage.state
}
stage.attributes.symbolize_keys.slice(*STAGE_ATTRIBUTES)
end
end
def groups_payload
tournament.stages.flat_map(&:groups).sort_by(&:id).map do |group|
{
id: group.id,
stage_id: group.stage_id,
number: group.number
}
group.attributes.symbolize_keys.slice(*GROUP_ATTRIBUTES)
end
end
def matches_payload
tournament.matches.sort_by(&:id).map do |match|
{
id: match.id,
stage_id: match.stage&.id,
group_id: match.group&.id,
state: match.state,
position: match.position,
decider: match.decider
}
match.attributes.symbolize_keys.slice(*MATCH_ATTRIBUTES)
end
end
def match_scores_payload
tournament.matches.flat_map(&:match_scores).sort_by(&:id).map do |match_score|
{
id: match_score.id,
match_id: match_score.match_id,
team_id: match_score.team_id,
points: match_score.points,
hidden_points: match_score.hidden_points
}
match_score.attributes.symbolize_keys.slice(*MATCH_SCORE_ATTRIBUTES)
end
end
def group_scores_payload
tournament.stages.flat_map(&:groups).flat_map(&:group_scores).sort_by(&:id).map do |group_score|
{
id: group_score.id,
group_id: group_score.group_id,
team_id: group_score.team_id,
group_points: group_score.group_points,
scored_points: group_score.scored_points,
received_points: group_score.received_points,
position: group_score.position
}
group_score.attributes.symbolize_keys.slice(*GROUP_SCORE_ATTRIBUTES)
end
end

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
class TournamentSyncSchema
MODEL_COLUMNS = {
Tournament => {
synced: %w[
id
name
code
description
public
playoff_teams_amount
instant_finalists_amount
intermediate_round_participants_amount
timer_end
].freeze,
ignored: %w[
user_id
created_at
updated_at
read_only_mode
sync_target_url
sync_auth_token
sync_source_tournament_id
sync_last_pushed_at
sync_last_push_error
sync_last_imported_snapshot_at
].freeze
}.freeze,
Team => {
synced: %w[id name].freeze,
ignored: %w[tournament_id created_at updated_at sync_source_id].freeze
}.freeze,
Stage => {
synced: %w[id level state].freeze,
ignored: %w[tournament_id created_at updated_at sync_source_id].freeze
}.freeze,
Group => {
synced: %w[id number stage_id].freeze,
ignored: %w[created_at updated_at sync_source_id].freeze
}.freeze,
Match => {
synced: %w[id state position stage_id group_id decider].freeze,
ignored: %w[created_at updated_at sync_source_id].freeze
}.freeze,
MatchScore => {
synced: %w[id points match_id team_id hidden_points].freeze,
ignored: %w[created_at updated_at sync_source_id].freeze
}.freeze,
GroupScore => {
synced: %w[id group_points scored_points received_points team_id group_id position].freeze,
ignored: %w[created_at updated_at sync_source_id].freeze
}.freeze
}.freeze
class << self
def synced_columns_for(model_class)
MODEL_COLUMNS.fetch(model_class).fetch(:synced)
end
def ignored_columns_for(model_class)
MODEL_COLUMNS.fetch(model_class).fetch(:ignored)
end
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe TournamentSyncSchema do
describe 'column coverage' do
described_class::MODEL_COLUMNS.each_key do |model_class|
it "accounts for every #{model_class.name} column in sync contract" do
synced_columns = described_class.synced_columns_for(model_class)
ignored_columns = described_class.ignored_columns_for(model_class)
accounted_columns = synced_columns + ignored_columns
expect(accounted_columns.uniq).to match_array(model_class.column_names)
expect(synced_columns & ignored_columns).to be_empty
end
end
end
end