jumbotron 0.1.6 → 0.2.0
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.
- checksums.yaml +4 -4
- data/app/adapters/jumbotron/adapters/espn/nfl.rb +15 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/scoreboard.rb +14 -2
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/teams.rb +2 -1
- data/app/adapters/jumbotron/adapters/sandbox/nfl.rb +135 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/acquisition_scope.rb +74 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/competition_odds.rb +82 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/current_observation_state.rb +53 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/line_acquisition_scope.rb +30 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/monotonic_observation.rb +113 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/observation_values.rb +53 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/outcome_candidates.rb +38 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/projection.rb +87 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/scoreboard.rb +115 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/synthetic_observation.rb +32 -0
- data/app/adapters/jumbotron/adapters/sandbox/nfl_helpers/timeline_observation.rb +119 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competitor.rb +14 -1
- data/app/deserializers/jumbotron/deserializers/clients/espn/competitor_record.rb +27 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/venue.rb +10 -1
- data/app/deserializers/jumbotron/deserializers/clients/espn/venue_address.rb +27 -0
- data/app/deserializers/jumbotron/deserializers/public/register_sandbox_projection.rb +42 -0
- data/app/deserializers/jumbotron/deserializers/public/reset_sandbox_projection.rb +51 -0
- data/app/errors/jumbotron/error.rb +7 -0
- data/app/errors/jumbotron/errors/sandbox.rb +67 -0
- data/app/errors/jumbotron/sandbox_reset_rejected_error.rb +6 -0
- data/app/models/jumbotron/canonical.rb +20 -5
- data/app/models/jumbotron/game.rb +11 -0
- data/app/models/jumbotron/public/translate_outcome.rb +13 -7
- data/app/models/jumbotron/public.rb +31 -6
- data/app/models/jumbotron/sandbox_game_mapping.rb +24 -0
- data/app/models/jumbotron/sandbox_game_plan.rb +23 -0
- data/app/models/jumbotron/sandbox_projection.rb +30 -0
- data/app/models/jumbotron/sandbox_projection_data.rb +32 -0
- data/app/models/jumbotron/season.rb +10 -0
- data/app/providers/jumbotron/providers/sandbox/resources/competition_odds.rb +24 -0
- data/app/providers/jumbotron/providers/sandbox/resources/projection.rb +23 -0
- data/app/providers/jumbotron/providers/sandbox/resources/scoreboard.rb +24 -0
- data/app/services/jumbotron/services/adapters/execute_line_acquisition_scope.rb +2 -1
- data/app/services/jumbotron/services/adapters/resolve_game_update_policy.rb +1 -0
- data/app/services/jumbotron/services/adapters/select_eligible_games.rb +1 -1
- data/app/services/jumbotron/services/canonical/upsert_game_graph.rb +126 -0
- data/app/services/jumbotron/services/canonical/upsert_game_participant.rb +114 -6
- data/app/services/jumbotron/services/canonical/upsert_team.rb +24 -0
- data/app/services/jumbotron/services/canonical/upsert_venue.rb +33 -1
- data/app/services/jumbotron/services/public/assemble_public_game.rb +20 -2
- data/app/services/jumbotron/services/public/assemble_sandbox_projection.rb +30 -0
- data/app/services/jumbotron/services/public/assemble_sandbox_projection_reset.rb +49 -0
- data/app/services/jumbotron/services/sandbox/build_source_snapshot.rb +224 -0
- data/app/services/jumbotron/services/sandbox/project_game.rb +73 -0
- data/app/services/jumbotron/services/sandbox/register_projection.rb +76 -0
- data/app/services/jumbotron/services/sandbox/remove_empty_projection_geometry.rb +46 -0
- data/app/services/jumbotron/services/sandbox/remove_mapped_game.rb +29 -0
- data/app/services/jumbotron/services/sandbox/resolve_game_observation.rb +106 -0
- data/app/services/jumbotron/services/sandbox/seed_projected_game.rb +95 -0
- data/app/services/jumbotron/services/sandbox/select_line_basis.rb +69 -0
- data/app/services/jumbotron/services/sandbox/validate_projected_season.rb +77 -0
- data/app/workflows/jumbotron/workflows/public_read.rb +2 -1
- data/app/workflows/jumbotron/workflows/sandbox/register_projection_workflow.rb +33 -0
- data/app/workflows/jumbotron/workflows/sandbox/reset_projection_workflow.rb +158 -0
- data/app/workflows/jumbotron/workflows/synchronize_adapter_workflow.rb +3 -51
- data/db/migrate/20260828220001_add_game_presentation_contract_fields.rb +19 -0
- data/db/migrate/20260908000001_create_jumbotron_sandbox_game_plans.rb +16 -0
- data/db/migrate/20260908000002_create_jumbotron_sandbox_projections.rb +39 -0
- data/docs/upgrades/0.2.0.md +74 -0
- data/lib/jumbotron/client.rb +14 -0
- data/lib/jumbotron/version.rb +1 -1
- data/spec/factories/jumbotron/sandbox_game_mapping.rb +24 -0
- data/spec/factories/jumbotron/sandbox_game_plan.rb +10 -0
- data/spec/factories/jumbotron/sandbox_projection.rb +18 -0
- metadata +62 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Jumbotron
|
|
6
|
+
module Adapters
|
|
7
|
+
module Sandbox
|
|
8
|
+
module NflHelpers
|
|
9
|
+
module Projection
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(payload, observed_at:)
|
|
13
|
+
data = payload.symbolize_keys
|
|
14
|
+
Canonical::SyncInput.new(
|
|
15
|
+
observed_at: observed_at,
|
|
16
|
+
games: [game_input(data)],
|
|
17
|
+
teams: []
|
|
18
|
+
)
|
|
19
|
+
rescue ArgumentError, KeyError, TypeError => e
|
|
20
|
+
raise TransformError, "invalid sandbox projection: #{e.message}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def game_input(data) # rubocop:disable Metrics/MethodLength
|
|
24
|
+
Canonical::GameInput.new(
|
|
25
|
+
provider_identities: game_identities(data),
|
|
26
|
+
scheduled_at: Time.iso8601(data.fetch(:scheduled_at).to_s),
|
|
27
|
+
lifecycle: "scheduled",
|
|
28
|
+
neutral_site: data.fetch(:neutral_site, false),
|
|
29
|
+
season_year: data.fetch(:season),
|
|
30
|
+
season_phase_key: data.fetch(:season_phase),
|
|
31
|
+
schedule_group: schedule_group(data),
|
|
32
|
+
venue: nil,
|
|
33
|
+
participants: %i[home away].map { |role| participant(data.fetch(role), role) },
|
|
34
|
+
progress: nil
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
private_class_method :game_input
|
|
38
|
+
|
|
39
|
+
def game_identities(data)
|
|
40
|
+
event_id = data.fetch(:event_id).to_s
|
|
41
|
+
competition_id = data.fetch(:competition_id, event_id).to_s
|
|
42
|
+
[
|
|
43
|
+
Canonical::ProviderIdentityRef.new(provider: "sandbox", namespace: "event", id: event_id),
|
|
44
|
+
Canonical::ProviderIdentityRef.new(
|
|
45
|
+
provider: "sandbox",
|
|
46
|
+
namespace: "competition",
|
|
47
|
+
id: competition_id
|
|
48
|
+
)
|
|
49
|
+
].uniq
|
|
50
|
+
end
|
|
51
|
+
private_class_method :game_identities
|
|
52
|
+
|
|
53
|
+
def schedule_group(data)
|
|
54
|
+
number = Integer(data.fetch(:week_number))
|
|
55
|
+
Canonical::ScheduleGroupInput.new(
|
|
56
|
+
kind: "week",
|
|
57
|
+
number: number,
|
|
58
|
+
name: data.fetch(:week_name).to_s
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
private_class_method :schedule_group
|
|
62
|
+
|
|
63
|
+
def participant(raw, role) # rubocop:disable Metrics/MethodLength
|
|
64
|
+
data = raw.symbolize_keys
|
|
65
|
+
Canonical::ParticipantInput.new(
|
|
66
|
+
provider_identities: [
|
|
67
|
+
Canonical::ProviderIdentityRef.new(
|
|
68
|
+
provider: "espn",
|
|
69
|
+
namespace: "team",
|
|
70
|
+
id: data.fetch(:provider_id).to_s
|
|
71
|
+
)
|
|
72
|
+
],
|
|
73
|
+
team_name: data.fetch(:name).to_s,
|
|
74
|
+
team_nickname: data[:nickname].presence,
|
|
75
|
+
team_abbreviation: data[:abbreviation].presence,
|
|
76
|
+
role: role.to_s,
|
|
77
|
+
score: nil,
|
|
78
|
+
result: nil,
|
|
79
|
+
record_summary: nil
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
private_class_method :participant
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Jumbotron
|
|
6
|
+
module Adapters
|
|
7
|
+
module Sandbox
|
|
8
|
+
module NflHelpers
|
|
9
|
+
class Scoreboard
|
|
10
|
+
def self.call(payload, observed_at:)
|
|
11
|
+
data = payload.symbolize_keys
|
|
12
|
+
resolved = resolve_observation(data.fetch(:game_id), observed_at)
|
|
13
|
+
Canonical::SyncInput.new(
|
|
14
|
+
observed_at: observed_at,
|
|
15
|
+
games: [game_input(data, resolved)],
|
|
16
|
+
teams: []
|
|
17
|
+
)
|
|
18
|
+
rescue ArgumentError, KeyError, TypeError => e
|
|
19
|
+
raise TransformError, "invalid sandbox scoreboard observation: #{e.message}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.resolve_observation(game_id, observed_at)
|
|
23
|
+
resolved = Jumbotron::Services::Sandbox::ResolveGameObservation.call(
|
|
24
|
+
game_id: game_id,
|
|
25
|
+
observed_at: observed_at
|
|
26
|
+
)
|
|
27
|
+
return resolved.data if resolved.success?
|
|
28
|
+
|
|
29
|
+
error = Array(resolved.errors).first
|
|
30
|
+
raise TransformError, error.respond_to?(:message) ? error.message : "sandbox observation failed"
|
|
31
|
+
end
|
|
32
|
+
private_class_method :resolve_observation
|
|
33
|
+
|
|
34
|
+
def self.game_input(data, resolved)
|
|
35
|
+
event_id = data.fetch(:event_id).to_s
|
|
36
|
+
competition_id = data[:competition_id].presence || event_id
|
|
37
|
+
Canonical::GameInput.new(**game_attributes(data, event_id, competition_id, resolved))
|
|
38
|
+
end
|
|
39
|
+
private_class_method :game_input
|
|
40
|
+
|
|
41
|
+
def self.game_attributes(data, event_id, competition_id, resolved) # rubocop:disable Metrics/MethodLength
|
|
42
|
+
observation = resolved.fetch(:observation)
|
|
43
|
+
{
|
|
44
|
+
provider_identities: game_identities(event_id, competition_id),
|
|
45
|
+
scheduled_at: resolved.fetch(:scheduled_at),
|
|
46
|
+
lifecycle: observation.lifecycle,
|
|
47
|
+
neutral_site: false,
|
|
48
|
+
season_year: season_identity(data.fetch(:season_year)),
|
|
49
|
+
season_phase_key: data.fetch(:season_phase_key).to_s,
|
|
50
|
+
schedule_group: schedule_group(data.fetch(:week_number)),
|
|
51
|
+
venue: nil,
|
|
52
|
+
participants: %i[home away].map do |role|
|
|
53
|
+
participant(data.fetch(role), role, observation)
|
|
54
|
+
end,
|
|
55
|
+
progress: observation.progress
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
private_class_method :game_attributes
|
|
59
|
+
|
|
60
|
+
def self.season_identity(value)
|
|
61
|
+
Integer(value, exception: false) || value.to_s
|
|
62
|
+
end
|
|
63
|
+
private_class_method :season_identity
|
|
64
|
+
|
|
65
|
+
def self.game_identities(event_id, competition_id)
|
|
66
|
+
[
|
|
67
|
+
Canonical::ProviderIdentityRef.new(provider: "sandbox", namespace: "event", id: event_id),
|
|
68
|
+
Canonical::ProviderIdentityRef.new(
|
|
69
|
+
provider: "sandbox",
|
|
70
|
+
namespace: "competition",
|
|
71
|
+
id: competition_id.to_s
|
|
72
|
+
)
|
|
73
|
+
].uniq
|
|
74
|
+
end
|
|
75
|
+
private_class_method :game_identities
|
|
76
|
+
|
|
77
|
+
def self.schedule_group(number)
|
|
78
|
+
week = Integer(number)
|
|
79
|
+
Canonical::ScheduleGroupInput.new(kind: "week", number: week, name: "Week #{week}")
|
|
80
|
+
end
|
|
81
|
+
private_class_method :schedule_group
|
|
82
|
+
|
|
83
|
+
def self.participant(raw, role, observation)
|
|
84
|
+
data = raw.symbolize_keys
|
|
85
|
+
Canonical::ParticipantInput.new(**participant_attributes(data, role, observation))
|
|
86
|
+
end
|
|
87
|
+
private_class_method :participant
|
|
88
|
+
|
|
89
|
+
def self.participant_attributes(data, role, observation)
|
|
90
|
+
{
|
|
91
|
+
provider_identities: [team_identity(data.fetch(:provider_id))],
|
|
92
|
+
team_name: data.fetch(:name).to_s,
|
|
93
|
+
team_nickname: data[:nickname].presence,
|
|
94
|
+
team_abbreviation: data[:abbreviation].presence,
|
|
95
|
+
role: role.to_s,
|
|
96
|
+
score: observation.public_send("#{role}_score"),
|
|
97
|
+
result: observation.public_send("#{role}_result"),
|
|
98
|
+
record_summary: nil
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
private_class_method :participant_attributes
|
|
102
|
+
|
|
103
|
+
def self.team_identity(provider_id)
|
|
104
|
+
Canonical::ProviderIdentityRef.new(
|
|
105
|
+
provider: "espn",
|
|
106
|
+
namespace: "team",
|
|
107
|
+
id: provider_id.to_s
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
private_class_method :team_identity
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
module Sandbox
|
|
6
|
+
module NflHelpers
|
|
7
|
+
module SyntheticObservation
|
|
8
|
+
Current = Data.define(:lifecycle, :progress, :home_score, :away_score)
|
|
9
|
+
Observation = Data.define(
|
|
10
|
+
:lifecycle,
|
|
11
|
+
:progress,
|
|
12
|
+
:home_score,
|
|
13
|
+
:away_score,
|
|
14
|
+
:home_result,
|
|
15
|
+
:away_result
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
QUARTER_SECONDS = TimelineObservation::QUARTER_SECONDS
|
|
19
|
+
HALFTIME_SECONDS = TimelineObservation::HALFTIME_SECONDS
|
|
20
|
+
TOTAL_SECONDS = TimelineObservation::TOTAL_SECONDS
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def call(now:, kickoff:, plan:, current:)
|
|
25
|
+
proposed = TimelineObservation.call(now: now, kickoff: kickoff, plan: plan)
|
|
26
|
+
MonotonicObservation.call(proposed: proposed, current: current, plan: plan)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
module Sandbox
|
|
6
|
+
module NflHelpers
|
|
7
|
+
module TimelineObservation
|
|
8
|
+
QUARTER_SECONDS = 27.5.minutes.to_i
|
|
9
|
+
HALFTIME_SECONDS = 10.minutes.to_i
|
|
10
|
+
ACTIVE_SECONDS = QUARTER_SECONDS * 4
|
|
11
|
+
TOTAL_SECONDS = ACTIVE_SECONDS + HALFTIME_SECONDS
|
|
12
|
+
GAME_CLOCK_SECONDS = 15.minutes.to_i
|
|
13
|
+
STAGES = [
|
|
14
|
+
[QUARTER_SECONDS, :q1],
|
|
15
|
+
[QUARTER_SECONDS * 2, :q2],
|
|
16
|
+
[(QUARTER_SECONDS * 2) + HALFTIME_SECONDS, :halftime],
|
|
17
|
+
[(QUARTER_SECONDS * 3) + HALFTIME_SECONDS, :q3],
|
|
18
|
+
[TOTAL_SECONDS, :q4]
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def call(now:, kickoff:, plan:)
|
|
24
|
+
elapsed = elapsed_seconds(now, kickoff)
|
|
25
|
+
return scheduled_observation if elapsed.negative?
|
|
26
|
+
return completed_observation(plan) if elapsed >= TOTAL_SECONDS
|
|
27
|
+
|
|
28
|
+
stage_observation(stage_for(elapsed), elapsed, plan)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def elapsed_seconds(now, kickoff)
|
|
32
|
+
now.to_time.to_f - kickoff.to_time.to_f
|
|
33
|
+
rescue ArgumentError, NoMethodError, TypeError
|
|
34
|
+
raise ArgumentError, "sandbox observation requires valid now and kickoff times"
|
|
35
|
+
end
|
|
36
|
+
private_class_method :elapsed_seconds
|
|
37
|
+
|
|
38
|
+
def stage_for(elapsed)
|
|
39
|
+
STAGES.find { |ending, _stage| elapsed < ending }.last
|
|
40
|
+
end
|
|
41
|
+
private_class_method :stage_for
|
|
42
|
+
|
|
43
|
+
def stage_observation(stage, elapsed, plan)
|
|
44
|
+
return halftime_observation(plan) if stage == :halftime
|
|
45
|
+
|
|
46
|
+
quarter = { q1: 1, q2: 2, q3: 3, q4: 4 }.fetch(stage)
|
|
47
|
+
local = local_elapsed(stage, elapsed)
|
|
48
|
+
active_observation(quarter, local, active_elapsed(stage, local), plan)
|
|
49
|
+
end
|
|
50
|
+
private_class_method :stage_observation
|
|
51
|
+
|
|
52
|
+
def local_elapsed(stage, elapsed)
|
|
53
|
+
offsets = {
|
|
54
|
+
q1: 0,
|
|
55
|
+
q2: QUARTER_SECONDS,
|
|
56
|
+
q3: (QUARTER_SECONDS * 2) + HALFTIME_SECONDS,
|
|
57
|
+
q4: (QUARTER_SECONDS * 3) + HALFTIME_SECONDS
|
|
58
|
+
}
|
|
59
|
+
elapsed - offsets.fetch(stage)
|
|
60
|
+
end
|
|
61
|
+
private_class_method :local_elapsed
|
|
62
|
+
|
|
63
|
+
def active_elapsed(stage, local)
|
|
64
|
+
completed_quarters = { q1: 0, q2: 1, q3: 2, q4: 3 }.fetch(stage)
|
|
65
|
+
(completed_quarters * QUARTER_SECONDS) + local
|
|
66
|
+
end
|
|
67
|
+
private_class_method :active_elapsed
|
|
68
|
+
|
|
69
|
+
def scheduled_observation
|
|
70
|
+
ObservationValues.observation(lifecycle: "scheduled")
|
|
71
|
+
end
|
|
72
|
+
private_class_method :scheduled_observation
|
|
73
|
+
|
|
74
|
+
def active_observation(quarter, local, active_elapsed, plan)
|
|
75
|
+
fraction = active_elapsed / ACTIVE_SECONDS.to_f
|
|
76
|
+
ObservationValues.observation(
|
|
77
|
+
lifecycle: "in_progress",
|
|
78
|
+
progress: ObservationValues.progress(state: "active", quarter: quarter, clock: clock(local)),
|
|
79
|
+
home_score: ObservationValues.score_at(plan.home_score, fraction),
|
|
80
|
+
away_score: ObservationValues.score_at(plan.away_score, fraction)
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
private_class_method :active_observation
|
|
84
|
+
|
|
85
|
+
def halftime_observation(plan)
|
|
86
|
+
ObservationValues.observation(
|
|
87
|
+
lifecycle: "in_progress",
|
|
88
|
+
progress: ObservationValues.progress(state: "intermission", quarter: 2),
|
|
89
|
+
home_score: ObservationValues.score_at(plan.home_score, 0.5),
|
|
90
|
+
away_score: ObservationValues.score_at(plan.away_score, 0.5)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
private_class_method :halftime_observation
|
|
94
|
+
|
|
95
|
+
def completed_observation(plan)
|
|
96
|
+
home_result, away_result = ObservationValues.results(plan.home_score, plan.away_score)
|
|
97
|
+
ObservationValues.observation(
|
|
98
|
+
lifecycle: "completed",
|
|
99
|
+
home_score: plan.home_score,
|
|
100
|
+
away_score: plan.away_score,
|
|
101
|
+
home_result: home_result,
|
|
102
|
+
away_result: away_result
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
private_class_method :completed_observation
|
|
106
|
+
|
|
107
|
+
def clock(local_seconds)
|
|
108
|
+
ObservationValues.clock(
|
|
109
|
+
local_seconds: local_seconds,
|
|
110
|
+
quarter_seconds: QUARTER_SECONDS,
|
|
111
|
+
game_clock_seconds: GAME_CLOCK_SECONDS
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
private_class_method :clock
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -13,6 +13,7 @@ module Jumbotron
|
|
|
13
13
|
field :winner, type: Support.types.boolean, nullable: true
|
|
14
14
|
field :order, type: Support.types.integer, nullable: true
|
|
15
15
|
field :team, type: Team::Result, required: true
|
|
16
|
+
field :records, type: Support.types.array(CompetitorRecord::Result), nullable: true
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def self.call(payload)
|
|
@@ -25,10 +26,22 @@ module Jumbotron
|
|
|
25
26
|
score: coerce_score(Support.payload.fetch(payload, "score")),
|
|
26
27
|
winner: Support.payload.fetch(payload, "winner"),
|
|
27
28
|
order: Support.payload.fetch(payload, "order"),
|
|
28
|
-
team: Support.nest("team", Support.payload.fetch(payload, "team")) { |raw| Team.call(raw) }
|
|
29
|
+
team: Support.nest("team", Support.payload.fetch(payload, "team")) { |raw| Team.call(raw) },
|
|
30
|
+
records: map_records(Support.payload.fetch(payload, "records"))
|
|
29
31
|
)
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
def self.map_records(raw)
|
|
35
|
+
return nil if raw.equal?(Support.missing) || raw.nil?
|
|
36
|
+
|
|
37
|
+
Array(raw).each_with_index.map do |item, index|
|
|
38
|
+
CompetitorRecord.call(item)
|
|
39
|
+
rescue CommandTower::Clients::Errors::DeserializationError => e
|
|
40
|
+
raise CommandTower::Deserializers::Clients::Errors.prefix(e, "records[#{index}]")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
private_class_method :map_records
|
|
44
|
+
|
|
32
45
|
def self.coerce_id(raw)
|
|
33
46
|
return raw if raw.equal?(Support.missing) || raw.nil? || raw.is_a?(String)
|
|
34
47
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Deserializers
|
|
5
|
+
module Clients
|
|
6
|
+
module Espn
|
|
7
|
+
class CompetitorRecord
|
|
8
|
+
class Result < CommandTower::Deserializers::Clients::Result
|
|
9
|
+
field :type, type: Support.types.string, required: true
|
|
10
|
+
field :summary, type: Support.types.string, required: true
|
|
11
|
+
field :name, type: Support.types.string, nullable: true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.call(payload)
|
|
15
|
+
Support.ensure_hash!(payload, label: "competitor.record")
|
|
16
|
+
|
|
17
|
+
Result.build!(
|
|
18
|
+
type: Support.payload.fetch(payload, "type"),
|
|
19
|
+
summary: Support.payload.fetch(payload, "summary"),
|
|
20
|
+
name: Support.payload.fetch(payload, "name")
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -9,6 +9,7 @@ module Jumbotron
|
|
|
9
9
|
field :id, type: Support.types.string, required: true
|
|
10
10
|
field :full_name, type: Support.types.string, required: true
|
|
11
11
|
field :indoor, type: Support.types.boolean, nullable: true
|
|
12
|
+
field :address, type: VenueAddress::Result, nullable: true
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def self.call(payload)
|
|
@@ -17,10 +18,18 @@ module Jumbotron
|
|
|
17
18
|
Result.build!(
|
|
18
19
|
id: coerce_id(Support.payload.fetch(payload, "id")),
|
|
19
20
|
full_name: Support.payload.fetch(payload, "fullName"),
|
|
20
|
-
indoor: Support.payload.fetch(payload, "indoor")
|
|
21
|
+
indoor: Support.payload.fetch(payload, "indoor"),
|
|
22
|
+
address: map_address(Support.payload.fetch(payload, "address"))
|
|
21
23
|
)
|
|
22
24
|
end
|
|
23
25
|
|
|
26
|
+
def self.map_address(raw)
|
|
27
|
+
return nil if raw.equal?(Support.missing) || raw.nil?
|
|
28
|
+
|
|
29
|
+
VenueAddress.call(raw)
|
|
30
|
+
end
|
|
31
|
+
private_class_method :map_address
|
|
32
|
+
|
|
24
33
|
def self.coerce_id(raw)
|
|
25
34
|
return raw if raw.equal?(Support.missing) || raw.nil? || raw.is_a?(String)
|
|
26
35
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Deserializers
|
|
5
|
+
module Clients
|
|
6
|
+
module Espn
|
|
7
|
+
class VenueAddress
|
|
8
|
+
class Result < CommandTower::Deserializers::Clients::Result
|
|
9
|
+
field :city, type: Support.types.string, nullable: true
|
|
10
|
+
field :state, type: Support.types.string, nullable: true
|
|
11
|
+
field :country, type: Support.types.string, nullable: true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.call(payload)
|
|
15
|
+
Support.ensure_hash!(payload, label: "venue.address")
|
|
16
|
+
|
|
17
|
+
Result.build!(
|
|
18
|
+
city: Support.payload.fetch(payload, "city"),
|
|
19
|
+
state: Support.payload.fetch(payload, "state"),
|
|
20
|
+
country: Support.payload.fetch(payload, "country")
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Deserializers
|
|
5
|
+
module Public
|
|
6
|
+
class RegisterSandboxProjection < CommandTower::Deserializers::ApplicationDeserializer
|
|
7
|
+
FIELDS = %i[sport league season season_phase].freeze
|
|
8
|
+
|
|
9
|
+
def call(params)
|
|
10
|
+
name = unwrap(require_string(unwrap(fetch_param(params, :name)), field: "name"))
|
|
11
|
+
return name if deserializer_result?(name)
|
|
12
|
+
|
|
13
|
+
source = unwrap(fetch_param(params, :source))
|
|
14
|
+
return source if deserializer_result?(source)
|
|
15
|
+
return invalid("source must be a hash") unless source.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
selector = parse_selector(source)
|
|
18
|
+
return selector if deserializer_result?(selector)
|
|
19
|
+
|
|
20
|
+
success(Jumbotron::Public::RegisterSandboxProjectionRequest.new(name: name, source: selector))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def parse_selector(source)
|
|
26
|
+
values = FIELDS.to_h do |field|
|
|
27
|
+
value = source[field] || source[field.to_s]
|
|
28
|
+
parsed = unwrap(require_string(value, field: "source.#{field}"))
|
|
29
|
+
return parsed if deserializer_result?(parsed)
|
|
30
|
+
|
|
31
|
+
[field, parsed]
|
|
32
|
+
end
|
|
33
|
+
Jumbotron::Public::SandboxScheduleSelector.new(**values)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def invalid(message)
|
|
37
|
+
failure(errors: [{ code: "invalid_request", message: message }])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Jumbotron
|
|
6
|
+
module Deserializers
|
|
7
|
+
module Public
|
|
8
|
+
class ResetSandboxProjection < CommandTower::Deserializers::ApplicationDeserializer
|
|
9
|
+
def call(params)
|
|
10
|
+
sandbox = unwrap(require_string(unwrap(fetch_param(params, :sandbox)), field: "sandbox"))
|
|
11
|
+
return sandbox if deserializer_result?(sandbox)
|
|
12
|
+
|
|
13
|
+
start_week = positive_integer(params, :start_week)
|
|
14
|
+
return start_week if deserializer_result?(start_week)
|
|
15
|
+
|
|
16
|
+
anchor_day = positive_integer(params, :anchor_day)
|
|
17
|
+
return anchor_day if deserializer_result?(anchor_day)
|
|
18
|
+
|
|
19
|
+
now = parse_time(unwrap(fetch_param(params, :now)))
|
|
20
|
+
return now if deserializer_result?(now)
|
|
21
|
+
|
|
22
|
+
success(
|
|
23
|
+
Jumbotron::Public::ResetSandboxProjectionRequest.new(
|
|
24
|
+
sandbox: sandbox,
|
|
25
|
+
start_week: start_week,
|
|
26
|
+
anchor_day: anchor_day,
|
|
27
|
+
now: now
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def positive_integer(params, field)
|
|
35
|
+
raw = unwrap(fetch_param(params, field))
|
|
36
|
+
return raw if deserializer_result?(raw)
|
|
37
|
+
|
|
38
|
+
unwrap(require_integer(raw, field: field.to_s, min: 1))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parse_time(raw)
|
|
42
|
+
return raw if raw.is_a?(Time) || raw.is_a?(ActiveSupport::TimeWithZone)
|
|
43
|
+
|
|
44
|
+
Time.iso8601(raw.to_s)
|
|
45
|
+
rescue ArgumentError, TypeError
|
|
46
|
+
failure(errors: [{ code: "invalid_request", message: "now must be an ISO8601 time" }])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Errors
|
|
5
|
+
module Sandbox
|
|
6
|
+
class InvalidGameError < CommandTower::Errors::ApplicationError
|
|
7
|
+
def code
|
|
8
|
+
"invalid_sandbox_game"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def message
|
|
12
|
+
"game is not an eligible sandbox game"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class OutcomeLineUnavailableError < CommandTower::Errors::ApplicationError
|
|
17
|
+
def code
|
|
18
|
+
"sandbox_outcome_line_unavailable"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def message
|
|
22
|
+
"sandbox outcome requires current home spread and over total lines"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class InvalidProgressionStateError < CommandTower::Errors::ApplicationError
|
|
27
|
+
def code
|
|
28
|
+
"invalid_sandbox_progression_state"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def message
|
|
32
|
+
details.is_a?(Hash) && details[:message].present? ? details[:message].to_s : "sandbox state cannot progress"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class ProjectionNotFoundError < CommandTower::Errors::ApplicationError
|
|
37
|
+
def code
|
|
38
|
+
"sandbox_not_found"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def message
|
|
42
|
+
"sandbox projection was not found"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class ProjectionConflictError < CommandTower::Errors::ApplicationError
|
|
47
|
+
def code
|
|
48
|
+
"sandbox_projection_conflict"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def message
|
|
52
|
+
"sandbox projection conflicts with its registered source"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class ProjectionRejectedError < CommandTower::Errors::ApplicationError
|
|
57
|
+
def code
|
|
58
|
+
details.is_a?(Hash) && details[:code].present? ? details[:code].to_s : "sandbox_projection_incomplete"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def message
|
|
62
|
+
details.is_a?(Hash) && details[:message].present? ? details[:message].to_s : "sandbox projection was rejected"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|