jumbotron 0.1.1

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.
Files changed (161) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.md +5 -0
  4. data/app/adapters/jumbotron/adapters/base.rb +136 -0
  5. data/app/adapters/jumbotron/adapters/cadence.rb +42 -0
  6. data/app/adapters/jumbotron/adapters/discovery_definition.rb +7 -0
  7. data/app/adapters/jumbotron/adapters/espn/nfl.rb +169 -0
  8. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/acquisition_scope.rb +29 -0
  9. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/competition_odds.rb +185 -0
  10. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/lifecycle.rb +30 -0
  11. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/line_acquisition_scope.rb +26 -0
  12. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/progress.rb +54 -0
  13. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/scoreboard.rb +146 -0
  14. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/season_phase.rb +33 -0
  15. data/app/adapters/jumbotron/adapters/espn/nfl_helpers/teams.rb +35 -0
  16. data/app/adapters/jumbotron/adapters/line_acquisition.rb +19 -0
  17. data/app/adapters/jumbotron/adapters/operation.rb +23 -0
  18. data/app/adapters/jumbotron/adapters/policy_definition.rb +14 -0
  19. data/app/adapters/jumbotron/adapters/registry.rb +78 -0
  20. data/app/adapters/jumbotron/adapters.rb +11 -0
  21. data/app/boot/jumbotron/boot/enqueue_discoveries.rb +41 -0
  22. data/app/clients/jumbotron/clients/espn/competition_odds/get.rb +26 -0
  23. data/app/clients/jumbotron/clients/espn/competition_odds/get_input.rb +16 -0
  24. data/app/clients/jumbotron/clients/espn/scoreboard/get.rb +18 -0
  25. data/app/clients/jumbotron/clients/espn/scoreboard/get_input.rb +17 -0
  26. data/app/clients/jumbotron/clients/espn/teams/get.rb +18 -0
  27. data/app/clients/jumbotron/clients/espn/teams/get_input.rb +15 -0
  28. data/app/clients/jumbotron/clients/espn.rb +44 -0
  29. data/app/clients/jumbotron/clients.rb +8 -0
  30. data/app/deserializers/jumbotron/deserializers/clients/espn/competition.rb +79 -0
  31. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/draw_odds.rb +31 -0
  32. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/get.rb +66 -0
  33. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/item.rb +60 -0
  34. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/listed_provider.rb +31 -0
  35. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/market_window.rb +31 -0
  36. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/point_spread.rb +27 -0
  37. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/price.rb +35 -0
  38. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/side_quote.rb +35 -0
  39. data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/team_odds.rb +40 -0
  40. data/app/deserializers/jumbotron/deserializers/clients/espn/competitor.rb +49 -0
  41. data/app/deserializers/jumbotron/deserializers/clients/espn/event.rb +81 -0
  42. data/app/deserializers/jumbotron/deserializers/clients/espn/scoreboard/get.rb +88 -0
  43. data/app/deserializers/jumbotron/deserializers/clients/espn/season.rb +27 -0
  44. data/app/deserializers/jumbotron/deserializers/clients/espn/status.rb +69 -0
  45. data/app/deserializers/jumbotron/deserializers/clients/espn/support.rb +61 -0
  46. data/app/deserializers/jumbotron/deserializers/clients/espn/team.rb +42 -0
  47. data/app/deserializers/jumbotron/deserializers/clients/espn/teams/get.rb +76 -0
  48. data/app/deserializers/jumbotron/deserializers/clients/espn/venue.rb +34 -0
  49. data/app/deserializers/jumbotron/deserializers/clients/espn/week.rb +23 -0
  50. data/app/deserializers/jumbotron/deserializers/public/game.rb +26 -0
  51. data/app/deserializers/jumbotron/deserializers/public/game_ids.rb +28 -0
  52. data/app/deserializers/jumbotron/deserializers/public/includes.rb +32 -0
  53. data/app/deserializers/jumbotron/deserializers/public/schedule.rb +151 -0
  54. data/app/deserializers/jumbotron/deserializers/public/schedule_groups.rb +91 -0
  55. data/app/errors/jumbotron/error.rb +6 -0
  56. data/app/errors/jumbotron/errors/adapters.rb +143 -0
  57. data/app/errors/jumbotron/errors/canonical.rb +67 -0
  58. data/app/errors/jumbotron/errors/public.rb +27 -0
  59. data/app/errors/jumbotron/invalid_request_error.rb +6 -0
  60. data/app/errors/jumbotron/not_found_error.rb +6 -0
  61. data/app/errors/jumbotron/unsupported_capability_error.rb +6 -0
  62. data/app/jobs/jumbotron/application_job.rb +6 -0
  63. data/app/jobs/jumbotron/discovery_job.rb +14 -0
  64. data/app/jobs/jumbotron/game_update_job.rb +12 -0
  65. data/app/jobs/jumbotron/line_update_job.rb +12 -0
  66. data/app/models/jumbotron/application_record.rb +8 -0
  67. data/app/models/jumbotron/bookmaker.rb +11 -0
  68. data/app/models/jumbotron/canonical.rb +128 -0
  69. data/app/models/jumbotron/game.rb +92 -0
  70. data/app/models/jumbotron/game_participant.rb +10 -0
  71. data/app/models/jumbotron/historical_change.rb +9 -0
  72. data/app/models/jumbotron/league.rb +12 -0
  73. data/app/models/jumbotron/line_observation.rb +18 -0
  74. data/app/models/jumbotron/observation_batch.rb +14 -0
  75. data/app/models/jumbotron/provider_identity.rb +32 -0
  76. data/app/models/jumbotron/public/translate_outcome.rb +57 -0
  77. data/app/models/jumbotron/public.rb +88 -0
  78. data/app/models/jumbotron/schedule_group.rb +22 -0
  79. data/app/models/jumbotron/season.rb +13 -0
  80. data/app/models/jumbotron/season_phase.rb +12 -0
  81. data/app/models/jumbotron/sport.rb +10 -0
  82. data/app/models/jumbotron/team.rb +11 -0
  83. data/app/models/jumbotron/venue.rb +10 -0
  84. data/app/providers/jumbotron/providers/espn/cooldown.rb +88 -0
  85. data/app/providers/jumbotron/providers/espn/cooldown_active_error.rb +13 -0
  86. data/app/providers/jumbotron/providers/espn/provider_failure.rb +40 -0
  87. data/app/providers/jumbotron/providers/espn/resources/competition_odds.rb +29 -0
  88. data/app/providers/jumbotron/providers/espn/resources/scoreboard.rb +90 -0
  89. data/app/providers/jumbotron/providers/espn/resources/teams.rb +19 -0
  90. data/app/scheduling/jumbotron/scheduling/backends/sidekiq_cron.rb +70 -0
  91. data/app/scheduling/jumbotron/scheduling/backends/solid_queue.rb +92 -0
  92. data/app/scheduling/jumbotron/scheduling/clock.rb +61 -0
  93. data/app/scheduling/jumbotron/scheduling/desired_schedules.rb +51 -0
  94. data/app/scheduling/jumbotron/scheduling/detect_integration.rb +45 -0
  95. data/app/scheduling/jumbotron/scheduling/materializer.rb +13 -0
  96. data/app/scheduling/jumbotron/scheduling/owned.rb +19 -0
  97. data/app/scheduling/jumbotron/scheduling/schedule_definition.rb +7 -0
  98. data/app/serializers/jumbotron/serializers/clients/espn/competition_odds/get.rb +17 -0
  99. data/app/serializers/jumbotron/serializers/clients/espn/scoreboard/get.rb +22 -0
  100. data/app/serializers/jumbotron/serializers/clients/espn/teams/get.rb +20 -0
  101. data/app/services/jumbotron/services/adapters/build_acquisition_scopes.rb +37 -0
  102. data/app/services/jumbotron/services/adapters/build_line_acquisition_scopes.rb +39 -0
  103. data/app/services/jumbotron/services/adapters/ensure_league.rb +29 -0
  104. data/app/services/jumbotron/services/adapters/execute_endpoint.rb +49 -0
  105. data/app/services/jumbotron/services/adapters/execute_line_acquisition_scope.rb +75 -0
  106. data/app/services/jumbotron/services/adapters/execute_synchronization_scope.rb +64 -0
  107. data/app/services/jumbotron/services/adapters/resolve_discovery.rb +53 -0
  108. data/app/services/jumbotron/services/adapters/resolve_game_update_policy.rb +74 -0
  109. data/app/services/jumbotron/services/adapters/resolve_line_update_policy.rb +74 -0
  110. data/app/services/jumbotron/services/adapters/select_eligible_games.rb +41 -0
  111. data/app/services/jumbotron/services/adapters/validate_sync_request.rb +51 -0
  112. data/app/services/jumbotron/services/canonical/attach_provider_identities.rb +59 -0
  113. data/app/services/jumbotron/services/canonical/derive_consensus_for_games.rb +18 -0
  114. data/app/services/jumbotron/services/canonical/derive_consensus_line.rb +70 -0
  115. data/app/services/jumbotron/services/canonical/ensure_bookmaker.rb +70 -0
  116. data/app/services/jumbotron/services/canonical/ensure_schedule_group.rb +40 -0
  117. data/app/services/jumbotron/services/canonical/ensure_season.rb +22 -0
  118. data/app/services/jumbotron/services/canonical/ensure_season_phase.rb +22 -0
  119. data/app/services/jumbotron/services/canonical/persist_line_observations.rb +145 -0
  120. data/app/services/jumbotron/services/canonical/record_observation_evidence.rb +51 -0
  121. data/app/services/jumbotron/services/canonical/resolve_current_lines.rb +21 -0
  122. data/app/services/jumbotron/services/canonical/resolve_current_lines_for_games.rb +69 -0
  123. data/app/services/jumbotron/services/canonical/upsert_game.rb +186 -0
  124. data/app/services/jumbotron/services/canonical/upsert_game_participant.rb +88 -0
  125. data/app/services/jumbotron/services/canonical/upsert_team.rb +90 -0
  126. data/app/services/jumbotron/services/canonical/upsert_venue.rb +92 -0
  127. data/app/services/jumbotron/services/public/assemble_public_game.rb +131 -0
  128. data/app/services/jumbotron/services/public/assemble_public_schedule.rb +63 -0
  129. data/app/services/jumbotron/services/public/derive_consensus_for_games.rb +21 -0
  130. data/app/services/jumbotron/services/public/load_games_by_ids.rb +18 -0
  131. data/app/services/jumbotron/services/public/load_included_lines.rb +47 -0
  132. data/app/services/jumbotron/services/public/map_public_consensus_lines.rb +44 -0
  133. data/app/services/jumbotron/services/public/map_public_current_lines.rb +27 -0
  134. data/app/services/jumbotron/services/public/read_game.rb +32 -0
  135. data/app/services/jumbotron/services/public/read_schedule.rb +116 -0
  136. data/app/services/jumbotron/services/public/read_schedule_groups.rb +90 -0
  137. data/app/services/jumbotron/services/public/resolve_current_lines_for_games.rb +21 -0
  138. data/app/synchronization/jumbotron/synchronization/lease.rb +127 -0
  139. data/app/workflows/jumbotron/workflows/execute_discovery_workflow.rb +131 -0
  140. data/app/workflows/jumbotron/workflows/execute_game_update_policy_workflow.rb +159 -0
  141. data/app/workflows/jumbotron/workflows/execute_line_update_policy_workflow.rb +156 -0
  142. data/app/workflows/jumbotron/workflows/public_read.rb +29 -0
  143. data/app/workflows/jumbotron/workflows/read_consensus_workflow.rb +37 -0
  144. data/app/workflows/jumbotron/workflows/read_current_lines_workflow.rb +27 -0
  145. data/app/workflows/jumbotron/workflows/read_game_workflow.rb +34 -0
  146. data/app/workflows/jumbotron/workflows/read_schedule_groups_workflow.rb +21 -0
  147. data/app/workflows/jumbotron/workflows/read_schedule_workflow.rb +48 -0
  148. data/app/workflows/jumbotron/workflows/synchronize_adapter_workflow.rb +183 -0
  149. data/db/migrate/20260812000001_create_jumbotron_foundational_catalog.rb +68 -0
  150. data/db/migrate/20260812000002_create_jumbotron_game_graph.rb +70 -0
  151. data/db/migrate/20260812000003_add_freshness_to_jumbotron_provider_mutated_entities.rb +31 -0
  152. data/db/migrate/20260812000004_create_jumbotron_observation_evidence.rb +34 -0
  153. data/db/migrate/20260813000001_create_jumbotron_schedule_groups.rb +24 -0
  154. data/db/migrate/20260814000001_create_jumbotron_line_observations.rb +38 -0
  155. data/db/migrate/20260820000001_add_progress_to_jumbotron_games.rb +14 -0
  156. data/lib/jumbotron/client.rb +40 -0
  157. data/lib/jumbotron/engine.rb +32 -0
  158. data/lib/jumbotron/version.rb +5 -0
  159. data/lib/jumbotron.rb +13 -0
  160. data/lib/tasks/jumbotron_schedules.rake +12 -0
  161. metadata +239 -0
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ class Competition
8
+ class Result < CommandTower::Deserializers::Clients::Result
9
+ field :id, type: Support.types.string, required: true
10
+ field :uid, type: Support.types.string, nullable: true
11
+ field :date, type: Support.types.string, nullable: true
12
+ field :start_date, type: Support.types.string, nullable: true
13
+ field :neutral_site, type: Support.types.boolean, nullable: true
14
+ field :status, type: Status::Result, required: true
15
+ field :venue, type: Venue::Result, nullable: true
16
+ field :competitors, type: Support.types.array(Competitor::Result), required: true
17
+ end
18
+
19
+ def self.call(payload)
20
+ Support.ensure_hash!(payload, label: "competition")
21
+
22
+ Result.build!(
23
+ id: coerce_id(Support.payload.fetch(payload, "id")),
24
+ uid: Support.payload.fetch(payload, "uid"),
25
+ date: Support.payload.fetch(payload, "date"),
26
+ start_date: Support.payload.fetch(payload, "startDate"),
27
+ neutral_site: Support.payload.fetch(payload, "neutralSite"),
28
+ status: Support.nest("status", Support.payload.fetch(payload, "status")) { |raw| Status.call(raw) },
29
+ venue: Support.nest("venue", Support.payload.fetch(payload, "venue")) { |raw| Venue.call(raw) },
30
+ competitors: map_competitors(Support.payload.fetch(payload, "competitors"))
31
+ )
32
+ end
33
+
34
+ def self.coerce_id(raw)
35
+ return raw if raw.equal?(Support.missing) || raw.nil? || raw.is_a?(String)
36
+
37
+ raw.to_s
38
+ end
39
+ private_class_method :coerce_id
40
+
41
+ def self.map_competitors(raw)
42
+ if raw.equal?(Support.missing)
43
+ raise CommandTower::Clients::Errors::DeserializationError.new(
44
+ message: "competitors is required",
45
+ details: {
46
+ path: "competitors",
47
+ expected: "Array",
48
+ actual: "missing",
49
+ rule: "required",
50
+ messages: ["competitors is required"]
51
+ }
52
+ )
53
+ end
54
+
55
+ unless raw.is_a?(Array)
56
+ raise CommandTower::Clients::Errors::DeserializationError.new(
57
+ message: "competitors must be an Array",
58
+ details: {
59
+ path: "competitors",
60
+ expected: "Array",
61
+ actual: raw.class.name,
62
+ rule: "type",
63
+ messages: ["competitors must be an Array"]
64
+ }
65
+ )
66
+ end
67
+
68
+ raw.each_with_index.map do |item, index|
69
+ Competitor.call(item)
70
+ rescue CommandTower::Clients::Errors::DeserializationError => e
71
+ raise CommandTower::Deserializers::Clients::Errors.prefix(e, "competitors[#{index}]")
72
+ end
73
+ end
74
+ private_class_method :map_competitors
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ module CompetitionOdds
8
+ class DrawOdds
9
+ class Result < CommandTower::Deserializers::Clients::Result
10
+ field :money_line, type: Support.types.float, nullable: true
11
+ field :summary, type: Support.types.string, nullable: true
12
+ field :value, type: Support.types.float, nullable: true
13
+ field :handicap, type: Support.types.float, nullable: true
14
+ end
15
+
16
+ def self.call(payload)
17
+ Support.ensure_hash!(payload, label: "draw_odds")
18
+
19
+ Result.build!(
20
+ money_line: Support.payload.fetch(payload, "moneyLine"),
21
+ summary: Support.payload.fetch(payload, "summary"),
22
+ value: Support.payload.fetch(payload, "value"),
23
+ handicap: Support.payload.fetch(payload, "handicap")
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ module CompetitionOdds
8
+ class Get
9
+ class Result < CommandTower::Deserializers::Clients::Result
10
+ field :count, type: Support.types.integer, nullable: true
11
+ field :page_index, type: Support.types.integer, nullable: true
12
+ field :page_size, type: Support.types.integer, nullable: true
13
+ field :page_count, type: Support.types.integer, nullable: true
14
+ field :items, type: Support.types.array(Item::Result), required: true
15
+ end
16
+
17
+ def self.call(payload)
18
+ Support.ensure_hash!(payload, label: "competition_odds")
19
+ items_raw = Support.payload.fetch(payload, "items")
20
+
21
+ if items_raw.equal?(Support.missing)
22
+ raise CommandTower::Clients::Errors::DeserializationError.new(
23
+ message: "items is required",
24
+ details: {
25
+ path: "items",
26
+ expected: "Array",
27
+ actual: "missing",
28
+ rule: "required",
29
+ messages: ["items is required"]
30
+ }
31
+ )
32
+ end
33
+
34
+ unless items_raw.is_a?(Array)
35
+ raise CommandTower::Clients::Errors::DeserializationError.new(
36
+ message: "items must be an Array",
37
+ details: {
38
+ path: "items",
39
+ expected: "Array",
40
+ actual: items_raw.class.name,
41
+ rule: "type",
42
+ messages: ["items must be an Array"]
43
+ }
44
+ )
45
+ end
46
+
47
+ items = items_raw.each_with_index.map do |item, index|
48
+ Item.call(item)
49
+ rescue CommandTower::Clients::Errors::DeserializationError => e
50
+ raise CommandTower::Deserializers::Clients::Errors.prefix(e, "items[#{index}]")
51
+ end
52
+
53
+ Result.build!(
54
+ count: Support.whole_number(Support.payload.fetch(payload, "count")),
55
+ page_index: Support.whole_number(Support.payload.fetch(payload, "pageIndex")),
56
+ page_size: Support.whole_number(Support.payload.fetch(payload, "pageSize")),
57
+ page_count: Support.whole_number(Support.payload.fetch(payload, "pageCount")),
58
+ items: items
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ module CompetitionOdds
8
+ class Item
9
+ class Result < CommandTower::Deserializers::Clients::Result
10
+ field :ref, type: Support.types.string, nullable: true
11
+ field :details, type: Support.types.string, nullable: true
12
+ field :spread, type: Support.types.float, nullable: true
13
+ field :over_under, type: Support.types.float, nullable: true
14
+ field :over_odds, type: Support.types.float, nullable: true
15
+ field :under_odds, type: Support.types.float, nullable: true
16
+ field :listed_provider, type: ListedProvider::Result, required: true
17
+ field :home_team_odds, type: TeamOdds::Result, nullable: true
18
+ field :away_team_odds, type: TeamOdds::Result, nullable: true
19
+ field :draw_odds, type: DrawOdds::Result, nullable: true
20
+ field :open, type: MarketWindow::Result, nullable: true
21
+ field :current, type: MarketWindow::Result, nullable: true
22
+ field :close, type: MarketWindow::Result, nullable: true
23
+ end
24
+
25
+ def self.call(payload)
26
+ Support.ensure_hash!(payload, label: "competition_odds_item")
27
+
28
+ Result.build!(
29
+ ref: Support.payload.fetch(payload, "$ref"),
30
+ details: Support.payload.fetch(payload, "details"),
31
+ spread: Support.payload.fetch(payload, "spread"),
32
+ over_under: Support.payload.fetch(payload, "overUnder"),
33
+ over_odds: Support.payload.fetch(payload, "overOdds"),
34
+ under_odds: Support.payload.fetch(payload, "underOdds"),
35
+ listed_provider: Support.nest("provider", Support.payload.fetch(payload, "provider")) do |raw|
36
+ ListedProvider.call(raw)
37
+ end,
38
+ home_team_odds: Support.nest("homeTeamOdds", Support.payload.fetch(payload, "homeTeamOdds")) do |raw|
39
+ TeamOdds.call(raw)
40
+ end,
41
+ away_team_odds: Support.nest("awayTeamOdds", Support.payload.fetch(payload, "awayTeamOdds")) do |raw|
42
+ TeamOdds.call(raw)
43
+ end,
44
+ draw_odds: Support.nest("drawOdds", Support.payload.fetch(payload, "drawOdds")) do |raw|
45
+ DrawOdds.call(raw)
46
+ end,
47
+ open: Support.nest("open", Support.payload.fetch(payload, "open")) { |raw| MarketWindow.call(raw) },
48
+ current: Support.nest(
49
+ "current",
50
+ Support.payload.fetch(payload, "current")
51
+ ) { |raw| MarketWindow.call(raw) },
52
+ close: Support.nest("close", Support.payload.fetch(payload, "close")) { |raw| MarketWindow.call(raw) }
53
+ )
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ module CompetitionOdds
8
+ class ListedProvider
9
+ class Result < CommandTower::Deserializers::Clients::Result
10
+ field :id, type: Support.types.string, required: true
11
+ field :name, type: Support.types.string, required: true
12
+ field :priority, type: Support.types.integer, nullable: true
13
+ field :ref, type: Support.types.string, nullable: true
14
+ end
15
+
16
+ def self.call(payload)
17
+ Support.ensure_hash!(payload, label: "listed_provider")
18
+
19
+ Result.build!(
20
+ id: Support.coerce_id(Support.payload.fetch(payload, "id")),
21
+ name: Support.payload.fetch(payload, "name"),
22
+ priority: Support.whole_number(Support.payload.fetch(payload, "priority")),
23
+ ref: Support.payload.fetch(payload, "$ref")
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ module CompetitionOdds
8
+ class MarketWindow
9
+ class Result < CommandTower::Deserializers::Clients::Result
10
+ field :over, type: Price::Result, nullable: true
11
+ field :under, type: Price::Result, nullable: true
12
+ field :total, type: PointSpread::Result, nullable: true
13
+ field :draw, type: Price::Result, nullable: true
14
+ end
15
+
16
+ def self.call(payload)
17
+ Support.ensure_hash!(payload, label: "market_window")
18
+
19
+ Result.build!(
20
+ over: Support.nest("over", Support.payload.fetch(payload, "over")) { |raw| Price.call(raw) },
21
+ under: Support.nest("under", Support.payload.fetch(payload, "under")) { |raw| Price.call(raw) },
22
+ total: Support.nest("total", Support.payload.fetch(payload, "total")) { |raw| PointSpread.call(raw) },
23
+ draw: Support.nest("draw", Support.payload.fetch(payload, "draw")) { |raw| Price.call(raw) }
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ module CompetitionOdds
8
+ class PointSpread
9
+ class Result < CommandTower::Deserializers::Clients::Result
10
+ field :american, type: Support.types.string, nullable: true
11
+ field :alternate_display_value, type: Support.types.string, nullable: true
12
+ end
13
+
14
+ def self.call(payload)
15
+ Support.ensure_hash!(payload, label: "point_spread")
16
+
17
+ Result.build!(
18
+ american: Support.payload.fetch(payload, "american"),
19
+ alternate_display_value: Support.payload.fetch(payload, "alternateDisplayValue")
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ module CompetitionOdds
8
+ class Price
9
+ class Result < CommandTower::Deserializers::Clients::Result
10
+ field :value, type: Support.types.float, nullable: true
11
+ field :display_value, type: Support.types.string, nullable: true
12
+ field :alternate_display_value, type: Support.types.string, nullable: true
13
+ field :decimal, type: Support.types.float, nullable: true
14
+ field :fraction, type: Support.types.string, nullable: true
15
+ field :american, type: Support.types.string, nullable: true
16
+ end
17
+
18
+ def self.call(payload)
19
+ Support.ensure_hash!(payload, label: "price")
20
+
21
+ Result.build!(
22
+ value: Support.payload.fetch(payload, "value"),
23
+ display_value: Support.payload.fetch(payload, "displayValue"),
24
+ alternate_display_value: Support.payload.fetch(payload, "alternateDisplayValue"),
25
+ decimal: Support.payload.fetch(payload, "decimal"),
26
+ fraction: Support.payload.fetch(payload, "fraction"),
27
+ american: Support.payload.fetch(payload, "american")
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ module CompetitionOdds
8
+ class SideQuote
9
+ class Result < CommandTower::Deserializers::Clients::Result
10
+ field :favorite, type: Support.types.boolean, nullable: true
11
+ field :point_spread, type: PointSpread::Result, nullable: true
12
+ field :spread, type: Price::Result, nullable: true
13
+ field :money_line, type: Price::Result, nullable: true
14
+ end
15
+
16
+ def self.call(payload)
17
+ Support.ensure_hash!(payload, label: "side_quote")
18
+
19
+ Result.build!(
20
+ favorite: Support.payload.fetch(payload, "favorite"),
21
+ point_spread: Support.nest("pointSpread", Support.payload.fetch(payload, "pointSpread")) do |raw|
22
+ PointSpread.call(raw)
23
+ end,
24
+ spread: Support.nest("spread", Support.payload.fetch(payload, "spread")) { |raw| Price.call(raw) },
25
+ money_line: Support.nest("moneyLine", Support.payload.fetch(payload, "moneyLine")) do |raw|
26
+ Price.call(raw)
27
+ end
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ module CompetitionOdds
8
+ class TeamOdds
9
+ class Result < CommandTower::Deserializers::Clients::Result
10
+ field :favorite, type: Support.types.boolean, nullable: true
11
+ field :underdog, type: Support.types.boolean, nullable: true
12
+ field :money_line, type: Support.types.float, nullable: true
13
+ field :spread_odds, type: Support.types.float, nullable: true
14
+ field :open, type: SideQuote::Result, nullable: true
15
+ field :current, type: SideQuote::Result, nullable: true
16
+ field :close, type: SideQuote::Result, nullable: true
17
+ end
18
+
19
+ def self.call(payload)
20
+ Support.ensure_hash!(payload, label: "team_odds")
21
+
22
+ Result.build!(
23
+ favorite: Support.payload.fetch(payload, "favorite"),
24
+ underdog: Support.payload.fetch(payload, "underdog"),
25
+ money_line: Support.payload.fetch(payload, "moneyLine"),
26
+ spread_odds: Support.payload.fetch(payload, "spreadOdds"),
27
+ open: Support.nest("open", Support.payload.fetch(payload, "open")) { |raw| SideQuote.call(raw) },
28
+ current: Support.nest(
29
+ "current",
30
+ Support.payload.fetch(payload, "current")
31
+ ) { |raw| SideQuote.call(raw) },
32
+ close: Support.nest("close", Support.payload.fetch(payload, "close")) { |raw| SideQuote.call(raw) }
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ class Competitor
8
+ class Result < CommandTower::Deserializers::Clients::Result
9
+ field :id, type: Support.types.string, required: true
10
+ field :uid, type: Support.types.string, nullable: true
11
+ field :home_away, type: Support.types.string, required: true
12
+ field :score, type: Support.types.string, nullable: true
13
+ field :winner, type: Support.types.boolean, nullable: true
14
+ field :order, type: Support.types.integer, nullable: true
15
+ field :team, type: Team::Result, required: true
16
+ end
17
+
18
+ def self.call(payload)
19
+ Support.ensure_hash!(payload, label: "competitor")
20
+
21
+ Result.build!(
22
+ id: coerce_id(Support.payload.fetch(payload, "id")),
23
+ uid: Support.payload.fetch(payload, "uid"),
24
+ home_away: Support.payload.fetch(payload, "homeAway"),
25
+ score: coerce_score(Support.payload.fetch(payload, "score")),
26
+ winner: Support.payload.fetch(payload, "winner"),
27
+ order: Support.payload.fetch(payload, "order"),
28
+ team: Support.nest("team", Support.payload.fetch(payload, "team")) { |raw| Team.call(raw) }
29
+ )
30
+ end
31
+
32
+ def self.coerce_id(raw)
33
+ return raw if raw.equal?(Support.missing) || raw.nil? || raw.is_a?(String)
34
+
35
+ raw.to_s
36
+ end
37
+ private_class_method :coerce_id
38
+
39
+ def self.coerce_score(raw)
40
+ return raw if raw.equal?(Support.missing) || raw.nil? || raw.is_a?(String)
41
+
42
+ raw.to_s
43
+ end
44
+ private_class_method :coerce_score
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ class Event
8
+ class Result < CommandTower::Deserializers::Clients::Result
9
+ field :id, type: Support.types.string, required: true
10
+ field :uid, type: Support.types.string, nullable: true
11
+ field :date, type: Support.types.string, required: true
12
+ field :name, type: Support.types.string, required: true
13
+ field :short_name, type: Support.types.string, nullable: true
14
+ field :season, type: Season::Result, nullable: true
15
+ field :week, type: Week::Result, nullable: true
16
+ field :status, type: Status::Result, nullable: true
17
+ field :competitions, type: Support.types.array(Competition::Result), required: true
18
+ end
19
+
20
+ def self.call(payload)
21
+ Support.ensure_hash!(payload, label: "event")
22
+
23
+ Result.build!(
24
+ id: coerce_id(Support.payload.fetch(payload, "id")),
25
+ uid: Support.payload.fetch(payload, "uid"),
26
+ date: Support.payload.fetch(payload, "date"),
27
+ name: Support.payload.fetch(payload, "name"),
28
+ short_name: Support.payload.fetch(payload, "shortName"),
29
+ season: Support.nest("season", Support.payload.fetch(payload, "season")) { |raw| Season.call(raw) },
30
+ week: Support.nest("week", Support.payload.fetch(payload, "week")) { |raw| Week.call(raw) },
31
+ status: Support.nest("status", Support.payload.fetch(payload, "status")) { |raw| Status.call(raw) },
32
+ competitions: map_competitions(Support.payload.fetch(payload, "competitions"))
33
+ )
34
+ end
35
+
36
+ def self.coerce_id(raw)
37
+ return raw if raw.equal?(Support.missing) || raw.nil? || raw.is_a?(String)
38
+
39
+ raw.to_s
40
+ end
41
+ private_class_method :coerce_id
42
+
43
+ def self.map_competitions(raw)
44
+ if raw.equal?(Support.missing)
45
+ raise CommandTower::Clients::Errors::DeserializationError.new(
46
+ message: "competitions is required",
47
+ details: {
48
+ path: "competitions",
49
+ expected: "Array",
50
+ actual: "missing",
51
+ rule: "required",
52
+ messages: ["competitions is required"]
53
+ }
54
+ )
55
+ end
56
+
57
+ unless raw.is_a?(Array)
58
+ raise CommandTower::Clients::Errors::DeserializationError.new(
59
+ message: "competitions must be an Array",
60
+ details: {
61
+ path: "competitions",
62
+ expected: "Array",
63
+ actual: raw.class.name,
64
+ rule: "type",
65
+ messages: ["competitions must be an Array"]
66
+ }
67
+ )
68
+ end
69
+
70
+ raw.each_with_index.map do |item, index|
71
+ Competition.call(item)
72
+ rescue CommandTower::Clients::Errors::DeserializationError => e
73
+ raise CommandTower::Deserializers::Clients::Errors.prefix(e, "competitions[#{index}]")
74
+ end
75
+ end
76
+ private_class_method :map_competitions
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ module Scoreboard
8
+ class Get
9
+ class CalendarPhase < CommandTower::Deserializers::Clients::Result
10
+ field :season_type, type: Support.types.integer, required: true
11
+ field :weeks, type: Support.types.array(Support.types.integer), required: true
12
+ end
13
+
14
+ class Result < CommandTower::Deserializers::Clients::Result
15
+ field :events, type: Support.types.array(Event::Result), required: true
16
+ field :season_calendar, type: Support.types.array(CalendarPhase), nullable: true
17
+ end
18
+
19
+ def self.call(payload)
20
+ Support.ensure_hash!(payload, label: "scoreboard")
21
+ events_raw = Support.payload.fetch(payload, "events")
22
+
23
+ if events_raw.equal?(Support.missing)
24
+ raise CommandTower::Clients::Errors::DeserializationError.new(
25
+ message: "events is required",
26
+ details: {
27
+ path: "events",
28
+ expected: "Array",
29
+ actual: "missing",
30
+ rule: "required",
31
+ messages: ["events is required"]
32
+ }
33
+ )
34
+ end
35
+
36
+ unless events_raw.is_a?(Array)
37
+ raise CommandTower::Clients::Errors::DeserializationError.new(
38
+ message: "events must be an Array",
39
+ details: {
40
+ path: "events",
41
+ expected: "Array",
42
+ actual: events_raw.class.name,
43
+ rule: "type",
44
+ messages: ["events must be an Array"]
45
+ }
46
+ )
47
+ end
48
+
49
+ events = events_raw.each_with_index.map do |item, index|
50
+ Event.call(item)
51
+ rescue CommandTower::Clients::Errors::DeserializationError => e
52
+ raise CommandTower::Deserializers::Clients::Errors.prefix(e, "events[#{index}]")
53
+ end
54
+
55
+ Result.build!(events: events, season_calendar: parse_season_calendar(payload))
56
+ end
57
+
58
+ def self.parse_season_calendar(payload)
59
+ leagues = payload["leagues"]
60
+ return [] unless leagues.is_a?(Array) && leagues.first.is_a?(Hash)
61
+
62
+ calendar = leagues.first["calendar"]
63
+ return [] unless calendar.is_a?(Array)
64
+
65
+ calendar.filter_map do |entry|
66
+ next unless entry.is_a?(Hash)
67
+
68
+ season_type = entry["value"].to_i
69
+ next if season_type <= 0 || season_type == 4
70
+
71
+ weeks = Array(entry["entries"]).filter_map do |week|
72
+ next unless week.is_a?(Hash)
73
+
74
+ number = week["value"].to_i
75
+ number if number.positive?
76
+ end.uniq
77
+ next if weeks.empty?
78
+
79
+ CalendarPhase.build!(season_type: season_type, weeks: weeks)
80
+ end
81
+ end
82
+ private_class_method :parse_season_calendar
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end