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,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ class Season
8
+ class Result < CommandTower::Deserializers::Clients::Result
9
+ field :year, type: Support.types.integer, required: true
10
+ field :type, type: Support.types.integer, required: true
11
+ field :slug, type: Support.types.string, nullable: true
12
+ end
13
+
14
+ def self.call(payload)
15
+ Support.ensure_hash!(payload, label: "season")
16
+
17
+ Result.build!(
18
+ year: Support.payload.fetch(payload, "year"),
19
+ type: Support.payload.fetch(payload, "type"),
20
+ slug: Support.payload.fetch(payload, "slug")
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ class Status
8
+ class Result < CommandTower::Deserializers::Clients::Result
9
+ field :clock, type: Support.types.union(Support.types.integer, Support.types.string), nullable: true
10
+ field :display_clock, type: Support.types.string, nullable: true
11
+ field :period, type: Support.types.integer, nullable: true
12
+ field :type_id, type: Support.types.string, nullable: true
13
+ field :type_name, type: Support.types.string, required: true
14
+ field :type_state, type: Support.types.string, required: true
15
+ field :type_completed, type: Support.types.boolean, required: true
16
+ field :type_description, type: Support.types.string, nullable: true
17
+ field :type_detail, type: Support.types.string, nullable: true
18
+ field :type_short_detail, type: Support.types.string, nullable: true
19
+ end
20
+
21
+ def self.call(payload)
22
+ Support.ensure_hash!(payload, label: "status")
23
+ type = fetch_type!(payload)
24
+
25
+ Result.build!(
26
+ clock: Support.whole_number(Support.payload.fetch(payload, "clock")),
27
+ display_clock: Support.payload.fetch(payload, "displayClock"),
28
+ period: Support.payload.fetch(payload, "period"),
29
+ type_id: coerce_string(Support.payload.fetch(type, "id")),
30
+ type_name: Support.payload.fetch(type, "name"),
31
+ type_state: Support.payload.fetch(type, "state"),
32
+ type_completed: Support.payload.fetch(type, "completed"),
33
+ type_description: Support.payload.fetch(type, "description"),
34
+ type_detail: Support.payload.fetch(type, "detail"),
35
+ type_short_detail: Support.payload.fetch(type, "shortDetail")
36
+ )
37
+ end
38
+
39
+ def self.fetch_type!(payload)
40
+ raw = Support.payload.fetch(payload, "type")
41
+ if raw.equal?(Support.missing) || raw.nil?
42
+ raise CommandTower::Clients::Errors::DeserializationError.new(
43
+ message: "status.type is required",
44
+ details: {
45
+ path: "type",
46
+ expected: "Hash",
47
+ actual: "missing",
48
+ rule: "required",
49
+ messages: ["status.type is required"]
50
+ }
51
+ )
52
+ end
53
+
54
+ Support.ensure_hash!(raw, label: "status.type")
55
+ raw
56
+ end
57
+ private_class_method :fetch_type!
58
+
59
+ def self.coerce_string(raw)
60
+ return raw if raw.equal?(Support.missing) || raw.nil? || raw.is_a?(String)
61
+
62
+ raw.to_s
63
+ end
64
+ private_class_method :coerce_string
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ module Support
8
+ module_function
9
+
10
+ def types
11
+ CommandTower::Deserializers::Clients::Types
12
+ end
13
+
14
+ def payload
15
+ CommandTower::Deserializers::Clients::Payload
16
+ end
17
+
18
+ def missing
19
+ CommandTower::Deserializers::Clients::Missing
20
+ end
21
+
22
+ def ensure_hash!(value, label:)
23
+ return if value.is_a?(Hash)
24
+
25
+ raise CommandTower::Clients::Errors::DeserializationError.new(
26
+ message: "#{label} payload must be a Hash",
27
+ details: {
28
+ path: "",
29
+ expected: "Hash",
30
+ actual: value.class.name,
31
+ rule: "type",
32
+ messages: ["#{label} payload must be a Hash"]
33
+ }
34
+ )
35
+ end
36
+
37
+ def nest(key, raw)
38
+ return raw if raw.equal?(missing) || raw.nil?
39
+
40
+ yield raw
41
+ rescue CommandTower::Clients::Errors::DeserializationError => e
42
+ raise CommandTower::Deserializers::Clients::Errors.prefix(e, key)
43
+ end
44
+
45
+ def whole_number(raw)
46
+ return raw if raw.equal?(missing) || raw.nil?
47
+ return raw.to_i if raw.is_a?(Float) && raw == raw.to_i
48
+
49
+ raw
50
+ end
51
+
52
+ def coerce_id(raw)
53
+ return raw if raw.equal?(missing) || raw.nil? || raw.is_a?(String)
54
+
55
+ raw.to_s
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ class Team
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 :abbreviation, type: Support.types.string, nullable: true
12
+ field :display_name, type: Support.types.string, required: true
13
+ field :short_display_name, type: Support.types.string, nullable: true
14
+ field :name, type: Support.types.string, nullable: true
15
+ field :location, type: Support.types.string, nullable: true
16
+ end
17
+
18
+ def self.call(payload)
19
+ Support.ensure_hash!(payload, label: "team")
20
+
21
+ Result.build!(
22
+ id: coerce_id(Support.payload.fetch(payload, "id")),
23
+ uid: Support.payload.fetch(payload, "uid"),
24
+ abbreviation: Support.payload.fetch(payload, "abbreviation"),
25
+ display_name: Support.payload.fetch(payload, "displayName"),
26
+ short_display_name: Support.payload.fetch(payload, "shortDisplayName"),
27
+ name: Support.payload.fetch(payload, "name"),
28
+ location: Support.payload.fetch(payload, "location")
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
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ module Teams
8
+ class Get
9
+ def self.call(payload)
10
+ Support.ensure_hash!(payload, label: "teams")
11
+
12
+ teams_raw = extract_team_entries(payload)
13
+ teams_raw.each_with_index.map do |entry, index|
14
+ team_payload = entry.is_a?(Hash) ? (entry["team"] || entry) : entry
15
+ Team.call(team_payload)
16
+ rescue CommandTower::Clients::Errors::DeserializationError => e
17
+ raise CommandTower::Deserializers::Clients::Errors.prefix(e, "teams[#{index}]")
18
+ end
19
+ end
20
+
21
+ def self.extract_team_entries(payload)
22
+ sports = Support.payload.fetch(payload, "sports")
23
+ if sports.equal?(Support.missing) || !sports.is_a?(Array) || sports.empty?
24
+ raise CommandTower::Clients::Errors::DeserializationError.new(
25
+ message: "sports[0] is required",
26
+ details: {
27
+ path: "sports",
28
+ expected: "Array",
29
+ actual: sports.equal?(Support.missing) ? "missing" : sports.class.name,
30
+ rule: "required",
31
+ messages: ["sports[0] is required"]
32
+ }
33
+ )
34
+ end
35
+
36
+ sport = sports.first
37
+ Support.ensure_hash!(sport, label: "sports[0]")
38
+ leagues = Support.payload.fetch(sport, "leagues")
39
+ if leagues.equal?(Support.missing) || !leagues.is_a?(Array) || leagues.empty?
40
+ raise CommandTower::Clients::Errors::DeserializationError.new(
41
+ message: "sports[0].leagues[0] is required",
42
+ details: {
43
+ path: "sports[0].leagues",
44
+ expected: "Array",
45
+ actual: leagues.equal?(Support.missing) ? "missing" : leagues.class.name,
46
+ rule: "required",
47
+ messages: ["sports[0].leagues[0] is required"]
48
+ }
49
+ )
50
+ end
51
+
52
+ league = leagues.first
53
+ Support.ensure_hash!(league, label: "sports[0].leagues[0]")
54
+ teams = Support.payload.fetch(league, "teams")
55
+ if teams.equal?(Support.missing) || !teams.is_a?(Array)
56
+ raise CommandTower::Clients::Errors::DeserializationError.new(
57
+ message: "sports[0].leagues[0].teams is required",
58
+ details: {
59
+ path: "sports[0].leagues[0].teams",
60
+ expected: "Array",
61
+ actual: teams.equal?(Support.missing) ? "missing" : teams.class.name,
62
+ rule: "required",
63
+ messages: ["sports[0].leagues[0].teams is required"]
64
+ }
65
+ )
66
+ end
67
+
68
+ teams
69
+ end
70
+ private_class_method :extract_team_entries
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ class Venue
8
+ class Result < CommandTower::Deserializers::Clients::Result
9
+ field :id, type: Support.types.string, required: true
10
+ field :full_name, type: Support.types.string, required: true
11
+ field :indoor, type: Support.types.boolean, nullable: true
12
+ end
13
+
14
+ def self.call(payload)
15
+ Support.ensure_hash!(payload, label: "venue")
16
+
17
+ Result.build!(
18
+ id: coerce_id(Support.payload.fetch(payload, "id")),
19
+ full_name: Support.payload.fetch(payload, "fullName"),
20
+ indoor: Support.payload.fetch(payload, "indoor")
21
+ )
22
+ end
23
+
24
+ def self.coerce_id(raw)
25
+ return raw if raw.equal?(Support.missing) || raw.nil? || raw.is_a?(String)
26
+
27
+ raw.to_s
28
+ end
29
+ private_class_method :coerce_id
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Clients
6
+ module Espn
7
+ class Week
8
+ class Result < CommandTower::Deserializers::Clients::Result
9
+ field :number, type: Support.types.integer, required: true
10
+ end
11
+
12
+ def self.call(payload)
13
+ Support.ensure_hash!(payload, label: "week")
14
+
15
+ Result.build!(
16
+ number: Support.payload.fetch(payload, "number")
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Public
6
+ class Game < CommandTower::Deserializers::ApplicationDeserializer
7
+ def call(params)
8
+ id_raw = unwrap(fetch_param(params, :id))
9
+ return id_raw if deserializer_result?(id_raw)
10
+ return failure(errors: [{ code: "invalid_request", message: "id is required" }]) if id_raw.nil?
11
+
12
+ id = unwrap(require_integer(id_raw, field: "id", min: 1))
13
+ return id if deserializer_result?(id)
14
+
15
+ include_raw = unwrap(fetch_param(params, :include))
16
+ return include_raw if deserializer_result?(include_raw)
17
+
18
+ includes = unwrap(Includes.parse(include_raw))
19
+ return includes if deserializer_result?(includes)
20
+
21
+ success(Jumbotron::Public::GameRequest.new(id: id, includes: includes))
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Public
6
+ class GameIds < CommandTower::Deserializers::ApplicationDeserializer
7
+ def call(params)
8
+ raw = unwrap(fetch_param(params, :game_ids))
9
+ return raw if deserializer_result?(raw)
10
+ return failure(errors: [{ code: "invalid_request", message: "game_ids is required" }]) if raw.nil?
11
+ unless raw.is_a?(Array)
12
+ return failure(errors: [{ code: "invalid_request", message: "game_ids must be an array" }])
13
+ end
14
+
15
+ ids = []
16
+ raw.each do |item|
17
+ coerced = unwrap(require_integer(item, field: "game_ids", min: 1))
18
+ return coerced if deserializer_result?(coerced)
19
+
20
+ ids << coerced
21
+ end
22
+
23
+ success(Jumbotron::Public::GameIdsRequest.new(game_ids: ids.uniq))
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Public
6
+ module Includes
7
+ module_function
8
+
9
+ def parse(raw)
10
+ return CommandTower::Deserializers::CoercionResult.ok([]) if raw.nil?
11
+
12
+ list = raw.is_a?(Array) ? raw : [raw]
13
+ symbols = list.map { |item| item.is_a?(String) ? item.strip.to_sym : item }
14
+ unless symbols.all? { |item| item.is_a?(Symbol) && item != :"" }
15
+ return CommandTower::Deserializers::CoercionResult.fail(code: "invalid_request", field: "include")
16
+ end
17
+
18
+ unknown = symbols - Jumbotron::Public::ALLOWED_INCLUDES
19
+ if unknown.any?
20
+ return CommandTower::Deserializers::CoercionResult.fail(
21
+ code: "invalid_request",
22
+ field: "include",
23
+ details: { unknown: unknown }
24
+ )
25
+ end
26
+
27
+ CommandTower::Deserializers::CoercionResult.ok(symbols.uniq)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Public
6
+ class Schedule < CommandTower::Deserializers::ApplicationDeserializer
7
+ def call(params)
8
+ values = {}
9
+ %i[
10
+ league_id sport league season_id season season_phase schedule_group_id
11
+ group starts_at ends_at include
12
+ ].each do |key|
13
+ raw = unwrap(fetch_param(params, key))
14
+ return raw if deserializer_result?(raw)
15
+
16
+ values[key] = raw
17
+ end
18
+
19
+ league_id = optional_positive_integer(values[:league_id], "league_id")
20
+ return league_id if deserializer_result?(league_id)
21
+
22
+ season_id = optional_positive_integer(values[:season_id], "season_id")
23
+ return season_id if deserializer_result?(season_id)
24
+
25
+ schedule_group_id = optional_positive_integer(values[:schedule_group_id], "schedule_group_id")
26
+ return schedule_group_id if deserializer_result?(schedule_group_id)
27
+
28
+ sport = optional_name(values[:sport])
29
+ return sport if deserializer_result?(sport)
30
+
31
+ league = optional_name(values[:league])
32
+ return league if deserializer_result?(league)
33
+
34
+ season = optional_name(values[:season])
35
+ return season if deserializer_result?(season)
36
+
37
+ season_phase = optional_name(values[:season_phase])
38
+ return season_phase if deserializer_result?(season_phase)
39
+
40
+ group = parse_group(values[:group])
41
+ return group if deserializer_result?(group)
42
+
43
+ starts_at = parse_time(values[:starts_at], "starts_at")
44
+ return starts_at if deserializer_result?(starts_at)
45
+
46
+ ends_at = parse_time(values[:ends_at], "ends_at")
47
+ return ends_at if deserializer_result?(ends_at)
48
+
49
+ includes = unwrap(Includes.parse(values[:include]))
50
+ return includes if deserializer_result?(includes)
51
+
52
+ request = Jumbotron::Public::ScheduleRequest.new(
53
+ league_id: league_id,
54
+ sport: sport,
55
+ league: league,
56
+ season_id: season_id,
57
+ season: season,
58
+ season_phase: season_phase,
59
+ schedule_group_id: schedule_group_id,
60
+ group: group,
61
+ starts_at: starts_at,
62
+ ends_at: ends_at,
63
+ includes: includes
64
+ )
65
+ error = validate_modes(request)
66
+ return error if error
67
+
68
+ success(request)
69
+ end
70
+
71
+ private
72
+
73
+ def optional_positive_integer(raw, field)
74
+ return if raw.nil?
75
+
76
+ unwrap(require_integer(raw, field: field, min: 1))
77
+ end
78
+
79
+ def optional_name(raw)
80
+ return if raw.nil?
81
+
82
+ unwrap(require_string(raw, field: "name"))
83
+ end
84
+
85
+ def parse_group(raw)
86
+ return if raw.nil?
87
+ return failure(errors: [{ code: "invalid_request", message: "group must be a hash" }]) unless raw.is_a?(Hash)
88
+
89
+ hash = raw.respond_to?(:to_h) ? raw.to_h : raw
90
+ kind_raw = hash[:kind] || hash["kind"]
91
+ number_raw = hash[:number] || hash["number"]
92
+ kind = unwrap(require_string(kind_raw, field: "group.kind"))
93
+ return kind if deserializer_result?(kind)
94
+
95
+ number = unwrap(require_integer(number_raw, field: "group.number", min: 1))
96
+ return number if deserializer_result?(number)
97
+
98
+ { kind: kind, number: number }
99
+ end
100
+
101
+ def parse_time(raw, field)
102
+ return if raw.nil?
103
+ return raw if raw.is_a?(Time) || raw.is_a?(ActiveSupport::TimeWithZone)
104
+
105
+ failure(errors: [{ code: "invalid_request", message: "#{field} must be a Time" }])
106
+ end
107
+
108
+ def validate_modes(request)
109
+ has_league = request.league_id.present? || (request.sport.present? && request.league.present?)
110
+ unless has_league
111
+ return failure(
112
+ errors: [{ code: "invalid_request", message: "league_id or sport+league is required" }]
113
+ )
114
+ end
115
+
116
+ has_group = request.schedule_group_id.present? || request.group.present?
117
+ has_time = request.starts_at.present? && request.ends_at.present?
118
+ if has_group && has_time
119
+ return failure(
120
+ errors: [{ code: "invalid_request", message: "provide group or time range, not both" }]
121
+ )
122
+ end
123
+ if !has_group && !has_time
124
+ return failure(
125
+ errors: [{ code: "invalid_request", message: "provide group or starts_at/ends_at" }]
126
+ )
127
+ end
128
+ if has_time && (request.starts_at.blank? || request.ends_at.blank?)
129
+ return failure(
130
+ errors: [{ code: "invalid_request", message: "starts_at and ends_at are required together" }]
131
+ )
132
+ end
133
+ if has_group && request.schedule_group_id.blank?
134
+ unless request.season_id.present? || request.season.present?
135
+ return failure(
136
+ errors: [{ code: "invalid_request", message: "season_id or season is required for group queries" }]
137
+ )
138
+ end
139
+ if request.season_phase.blank?
140
+ return failure(
141
+ errors: [{ code: "invalid_request", message: "season_phase is required for group queries" }]
142
+ )
143
+ end
144
+ end
145
+
146
+ nil
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Public
6
+ class ScheduleGroups < CommandTower::Deserializers::ApplicationDeserializer
7
+ def call(params)
8
+ values = {}
9
+ %i[league_id sport league season_id season season_phase kind].each do |key|
10
+ raw = unwrap(fetch_param(params, key))
11
+ return raw if deserializer_result?(raw)
12
+
13
+ values[key] = raw
14
+ end
15
+
16
+ league_id = optional_positive_integer(values[:league_id], "league_id")
17
+ return league_id if deserializer_result?(league_id)
18
+
19
+ season_id = optional_positive_integer(values[:season_id], "season_id")
20
+ return season_id if deserializer_result?(season_id)
21
+
22
+ sport = optional_name(values[:sport])
23
+ return sport if deserializer_result?(sport)
24
+
25
+ league = optional_name(values[:league])
26
+ return league if deserializer_result?(league)
27
+
28
+ season = optional_name(values[:season])
29
+ return season if deserializer_result?(season)
30
+
31
+ season_phase = optional_name(values[:season_phase])
32
+ return season_phase if deserializer_result?(season_phase)
33
+
34
+ kind = optional_name(values[:kind])
35
+ return kind if deserializer_result?(kind)
36
+
37
+ request = Jumbotron::Public::ScheduleGroupsRequest.new(
38
+ league_id: league_id,
39
+ sport: sport,
40
+ league: league,
41
+ season_id: season_id,
42
+ season: season,
43
+ season_phase: season_phase,
44
+ kind: kind
45
+ )
46
+ error = validate_modes(request)
47
+ return error if error
48
+
49
+ success(request)
50
+ end
51
+
52
+ private
53
+
54
+ def optional_positive_integer(raw, field)
55
+ return if raw.nil?
56
+
57
+ unwrap(require_integer(raw, field: field, min: 1))
58
+ end
59
+
60
+ def optional_name(raw)
61
+ return if raw.nil?
62
+
63
+ unwrap(require_string(raw, field: "name"))
64
+ end
65
+
66
+ def validate_modes(request)
67
+ has_league = request.league_id.present? || (request.sport.present? && request.league.present?)
68
+ unless has_league
69
+ return failure(
70
+ errors: [{ code: "invalid_request", message: "league_id or sport+league is required" }]
71
+ )
72
+ end
73
+
74
+ unless request.season_id.present? || request.season.present?
75
+ return failure(
76
+ errors: [{ code: "invalid_request", message: "season_id or season is required" }]
77
+ )
78
+ end
79
+
80
+ if request.season_phase.blank?
81
+ return failure(
82
+ errors: [{ code: "invalid_request", message: "season_phase is required" }]
83
+ )
84
+ end
85
+
86
+ nil
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ class Error < StandardError
5
+ end
6
+ end