diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..02c73f2 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,100 @@ +# Copilot Code Review Guidelines + +--- +This file configures GitHub Copilot Code Review according to GitHub’s official guidance: [Use Copilot Code Review](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/request-a-code-review/use-code-review). + +## Project Context + +**Language:** Ruby ~> 4 +**Framework:** [trailblazer](https://trailblazer.to/) +**Testing:** [minitest](https://docs.seattlerb.org/minitest/) following [BetterSpecs](https://evenbetterspecs.github.io/) guidelines + +--- + +## Purpose & Scope + +Focused inline review guidance for Ruby pull requests—style, tests, performance, and tone. (CI setup, contributor workflows, and documentation policies belong in separate docs.) + +--- + +## 1. Style Guides + +> ⚠️ **Be constructive:** Frame style feedback as suggestions (e.g., “Consider using…”). + +All standard rules follow the [Shopify Ruby Style Guide](https://ruby-style-guide.shopify.dev/); only team-specific overrides are listed below: +- Indentation and line length enforced by RuboCop per project config. +- Use guard clauses, and explicit receivers. +- Prefer Railway Oriented Programming (ROP) for error handling and flow. +- Leverage dry-rb conventions and Trailblazer patterns for clarity and maintainability. +- Keep methods short. 1 line is fine. 5 is a lot. At 7, you should consider extracting a method. + +--- + +## 2. Tone and Delivery + +> 🤝 **Be succinct:** Don't overwhelm with too many minor issues; prioritize key improvements. + +- Use phrasing like “Consider extracting…” or “You might simplify…” rather than absolutes. +- Keep comments concise—2–3 sentences max. + +--- + +## 3. Conventions & Readability + +> ✨ **Keep it clear:** Propose naming and structure that clarify intent. + +- **SOLID principles:** Single responsibility; inject dependencies via initializers. +- **Descriptive names:** Recommend clear class, method, and variable names. +- **DRY:** Suggest extracting duplicate logic into modules or service objects. +- **Method size:** Flag methods >= 8 lines; suggest splitting into helpers. +- Ensure files have EOF newline. +- Flag trailing white space in code and comments. +- Always ensure rubocop runs cleanly. + +--- + +## 4. Testing and Coverage + +> 🧪 **Be thorough:** Highlight gaps in meaningful test coverage. + +- Flag missing or outdated specs for changed behaviors (unit, integration, request). +- Suggest tests for edge cases: error conditions, invalid inputs, boundary values, and potential `nil` handling or pointer exceptions. Follow best practices from the [BetterSpecs style guide](https://evenbetterspecs.github.io/). +- Encourage descriptive `context` blocks and example names per [BetterSpecs](https://evenbetterspecs.github.io/). +- Flag missing specs when logic is updated but no tests were added or modified +--- + +## 5. Performance and Security + +> 🚀 **Guard quality:** Call out patterns that may hurt performance or security. + +- Detect potential N+1 queries; suggest `includes` or batching. +- Attempt to identify O(n^2) or O(n log n) algorithms; recommend more efficient alternatives. +- Flag raw SQL, unsanitized interpolation, or other patterns that could introduce potential SQL injection vulnerabilities; recommend parameter binding and Sequel-based alternatives. Build these into a model, avoid raw sequel unless absolutely necessary. +- Identify unescaped user input in route methods + +--- + +## 6. Commit Message and PR title guidelines + +> 📝 **Be clear:** Ensure commit messages and PR titles reflect changes accurately. + +- We use Conventional Commits. Ensure commit messages follow the format: `(): `. +- We use Semantic PR titles as well, ensure PR titles also match the conventional commit format. + +## 7. Balancing Feedback + +> ⚖️ **Be selective:** Focus on the highest-impact issues. + +- Limit critical suggestions to the top 3–5 items per review. +- Combine minor style tweaks into a single comment when possible. + +--- + +## 8. Quick Reference + +- **Max comment length:** 2–3 sentences. +- **Severity tags:** `[Major]`, `[Medium]`, `[Minor]`. +- **Mandatory header:** `Enable frozen-string-literal`. +- **Top issues:** 3–5 critical comments. + +--- diff --git a/Readme.adoc b/Readme.adoc index bb14edb..2c04ed1 100644 --- a/Readme.adoc +++ b/Readme.adoc @@ -47,7 +47,7 @@ Access Polymarket endpoints via the `polymarket` namespace. ==== Markets -[API Reference](https://docs.domeapi.io/api-reference/endpoint/get-markets) +https://docs.domeapi.io/api-reference/endpoint/get-markets[API Reference] List markets with optional filtering: @@ -69,7 +69,7 @@ price = client.polymarket.markets.price(token_id: 'token_id') ==== Candlesticks -[API Reference](https://docs.domeapi.io/api-reference/endpoint/get-candlesticks) +https://docs.domeapi.io/api-reference/endpoint/get-candlesticks[API Reference] Get candlesticks: @@ -85,7 +85,7 @@ candlesticks = client.polymarket.candlesticks.list( ==== Trade History -[API Reference](https://docs.domeapi.io/api-reference/endpoint/get-trade-history) +https://docs.domeapi.io/api-reference/endpoint/get-trade-history[API Reference] Get trade history: @@ -99,7 +99,7 @@ trades = client.polymarket.trade_history.list( ==== Orderbook -[API Reference](https://docs.domeapi.io/api-reference/endpoint/get-orderbook-history) +https://docs.domeapi.io/api-reference/endpoint/get-orderbook-history[API Reference] Get orderbook history: @@ -114,7 +114,7 @@ snapshots = client.polymarket.orderbook.list( ==== Activity -[API Reference](https://docs.domeapi.io/api-reference/endpoint/get-activity) +https://docs.domeapi.io/api-reference/endpoint/get-activity[API Reference] Get activity: @@ -128,7 +128,7 @@ activity = client.polymarket.activity.list( ==== Market Price -[API Reference](https://docs.domeapi.io/api-reference/endpoint/get-market-price) +https://docs.domeapi.io/api-reference/endpoint/get-market-price[API Reference] Get market price history: @@ -142,7 +142,7 @@ prices = client.polymarket.market_price.list( ==== Wallet -[API Reference](https://docs.domeapi.io/api-reference/endpoint/get-wallet) +https://docs.domeapi.io/api-reference/endpoint/get-wallet[API Reference] Get wallet information: @@ -155,7 +155,7 @@ wallet = client.polymarket.wallet.list( ==== Wallet Profit and Loss -[API Reference](https://docs.domeapi.io/api-reference/endpoint/get-wallet-pnl) +https://docs.domeapi.io/api-reference/endpoint/get-wallet-pnl[API Reference] Get wallet profit and loss: @@ -167,6 +167,41 @@ pnl = client.polymarket.wallet_profit_and_loss.list( ) ---- +=== Matching Markets + +Access Matching Markets endpoints via the `matching_markets` namespace. + +==== Sports + +https://docs.domeapi.io/api-reference/endpoint/get-matching-markets-sports[API Reference] + +List matching markets for a specific sport and date: + +[source,ruby] +---- +# Using dynamic methods +markets = client.matching_markets.nfl_on(Date.today) +markets = client.matching_markets.nba_on('2023-12-25') + +# Using sports_by_date method +markets = client.matching_markets.sports_by_date(sport: 'nfl', date: Date.today) +---- + +==== Markets + +https://docs.domeapi.io/api-reference/endpoint/get-matching-markets[API Reference] + +List matching markets by slug or ticker: + +[source,ruby] +---- +# By Polymarket slug +markets = client.matching_markets.sports(polymarket_market_slug: 'super-bowl-lviii-winner') + +# By Kalshi ticker +markets = client.matching_markets.sports(kalshi_event_ticker: 'SB58') +---- + == Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. diff --git a/lib/domeapi/client.rb b/lib/domeapi/client.rb index 2a8eeb3..0968524 100644 --- a/lib/domeapi/client.rb +++ b/lib/domeapi/client.rb @@ -28,6 +28,10 @@ def polymarket @polymarket ||= Polymarket::Client.new(clone) end + def matching_markets + @matching_markets ||= MatchingMarkets.new(self) + end + private def full_url(path) diff --git a/lib/domeapi/matching_markets.rb b/lib/domeapi/matching_markets.rb new file mode 100644 index 0000000..ab3fde9 --- /dev/null +++ b/lib/domeapi/matching_markets.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module Rubyists + module Domeapi + # Matching Markets API endpoints + class MatchingMarkets + SPORTS = %w[nfl mlb cfb nba nhl cbb].freeze + + # Filter for matching markets sports + class SportsFilter < Contract + propertize(%i[sport date]) + + validation do + params do + required(:sport).filled(:string, included_in?: SPORTS) + required(:date).filled(:string, format?: /\A\d{4}-\d{2}-\d{2}\z/) + end + end + end + + # Filter for matching markets + class Filter < Contract + propertize(%i[polymarket_market_slug kalshi_event_ticker]) + + validation do + params do + optional(:polymarket_market_slug).maybe(Types::PolymarketMarketSlug) + optional(:kalshi_event_ticker).maybe(Types::KalshiEventTicker) + end + + rule(:polymarket_market_slug, :kalshi_event_ticker) do + if !values[:polymarket_market_slug] && !values[:kalshi_event_ticker] + key.failure('Either polymarket_market_slug or kalshi_event_ticker must be provided') + end + end + end + end + + attr_reader :client + + # @param client [Rubyists::Domeapi::Client] + # + # @return [void] + def initialize(client = Rubyists::Domeapi::Client.new) + @client = client + end + + # List matching markets + # + # @param filter [Filter|Hash] Filter options + # + # @return [Hash|Array] resource data + def sports(filter = Filter.new(Filter::Properties.new)) + filter = Filter.new(Filter::Properties.new(**filter)) if filter.is_a?(Hash) + raise ArgumentError, filter.errors.full_messages.join(', ') unless filter.validate({}) + + client.get('matching-markets/sports', params: filter.to_h) + end + + # List matching markets for a specific sport and date + # + # @param filter [SportsFilter|Hash] Filter options + # + # @return [Hash|Array] resource data + def sports_by_date(filter = SportsFilter.new(SportsFilter::Properties.new)) + filter = prepare_sports_filter(filter) + raise ArgumentError, filter.errors.full_messages.join(', ') unless filter.validate({}) + + client.get("matching-markets/sports/#{filter.sport}", params: { date: filter.date }) + end + + SPORTS.each do |sport| + define_method("#{sport}_on") do |date| + sports_by_date(sport: sport, date: date) + end + + define_singleton_method("#{sport}_on") do |date| + new.send("#{sport}_on", date) + end + end + + private + + def prepare_sports_filter(filter) + return filter unless filter.is_a?(Hash) + + filter[:date] = filter[:date].strftime('%Y-%m-%d') if filter[:date].respond_to?(:strftime) + SportsFilter.new(SportsFilter::Properties.new(**filter)) + end + end + end +end diff --git a/lib/domeapi/polymarket/activity.rb b/lib/domeapi/polymarket/activity.rb index 92df302..715fb8d 100644 --- a/lib/domeapi/polymarket/activity.rb +++ b/lib/domeapi/polymarket/activity.rb @@ -14,7 +14,6 @@ class Filter < Contract propertize(%i[user start_time end_time market_slug condition_id limit offset]) validation do - # :nocov: params do required(:user).filled(:string) optional(:start_time).maybe(:integer) @@ -24,7 +23,6 @@ class Filter < Contract optional(:limit).maybe(:integer, gteq?: 1, lteq?: 1000) optional(:offset).maybe(:integer, gteq?: 0) end - # :nocov: end end end diff --git a/lib/domeapi/polymarket/candlesticks.rb b/lib/domeapi/polymarket/candlesticks.rb index 69937ab..c8e3b2b 100644 --- a/lib/domeapi/polymarket/candlesticks.rb +++ b/lib/domeapi/polymarket/candlesticks.rb @@ -15,14 +15,12 @@ class Filter < Contract propertize(%i[condition_id start_time end_time interval]) validation do - # :nocov: params do required(:condition_id).filled(:string) required(:start_time).filled(:integer) required(:end_time).filled(:integer) optional(:interval).maybe(:integer, gteq?: 1, lteq?: 1440) end - # :nocov: end end end diff --git a/lib/domeapi/polymarket/market_price.rb b/lib/domeapi/polymarket/market_price.rb index 18abe83..2b22d97 100644 --- a/lib/domeapi/polymarket/market_price.rb +++ b/lib/domeapi/polymarket/market_price.rb @@ -14,12 +14,10 @@ class Filter < Contract propertize(%i[token_id at_time]) validation do - # :nocov: params do required(:token_id).filled(:string) optional(:at_time).maybe(:integer) end - # :nocov: end end end diff --git a/lib/domeapi/polymarket/markets.rb b/lib/domeapi/polymarket/markets.rb index d0b88c0..d7cb146 100644 --- a/lib/domeapi/polymarket/markets.rb +++ b/lib/domeapi/polymarket/markets.rb @@ -15,12 +15,10 @@ class Filter < Contract propertize(%i[market_slug event_slug condition_id tags status min_volume limit offset start_time end_time]) validation do - # :nocov: params do optional(:status).maybe(:string, included_in?: %w[open closed]) optional(:offset).maybe(:integer, gteq?: 0, lteq?: 100) end - # :nocov: end end diff --git a/lib/domeapi/polymarket/orderbook.rb b/lib/domeapi/polymarket/orderbook.rb index d3efa26..e315a7d 100644 --- a/lib/domeapi/polymarket/orderbook.rb +++ b/lib/domeapi/polymarket/orderbook.rb @@ -14,7 +14,6 @@ class Filter < Contract propertize(%i[token_id start_time end_time limit offset]) validation do - # :nocov: params do required(:token_id).filled(:string) required(:start_time).filled(:integer) @@ -22,7 +21,6 @@ class Filter < Contract optional(:limit).maybe(:integer, gteq?: 1, lteq?: 1000) optional(:offset).maybe(:integer, gteq?: 0) end - # :nocov: end end end diff --git a/lib/domeapi/polymarket/trade_history.rb b/lib/domeapi/polymarket/trade_history.rb index dbe5d30..e41e347 100644 --- a/lib/domeapi/polymarket/trade_history.rb +++ b/lib/domeapi/polymarket/trade_history.rb @@ -16,7 +16,6 @@ class Filter < Contract propertize(%i[market_slug condition_id token_id start_time end_time limit offset user]) validation do - # :nocov: params do optional(:market_slug).maybe(:string) optional(:condition_id).maybe(:string) @@ -27,7 +26,6 @@ class Filter < Contract optional(:offset).maybe(:integer, gteq?: 0) optional(:user).maybe(:string) end - # :nocov: end end end diff --git a/lib/domeapi/polymarket/wallet.rb b/lib/domeapi/polymarket/wallet.rb index c0fd6b8..99101ad 100644 --- a/lib/domeapi/polymarket/wallet.rb +++ b/lib/domeapi/polymarket/wallet.rb @@ -14,7 +14,6 @@ class Filter < Contract propertize(%i[eoa proxy with_metrics start_time end_time]) validation do - # :nocov: params do optional(:eoa).maybe(:string) optional(:proxy).maybe(:string) @@ -27,7 +26,6 @@ class Filter < Contract key.failure('Either eoa or proxy must be provided, but not both') if values[:eoa] && values[:proxy] key.failure('Either eoa or proxy must be provided') if !values[:eoa] && !values[:proxy] end - # :nocov: end end end diff --git a/lib/domeapi/polymarket/wallet_profit_and_loss.rb b/lib/domeapi/polymarket/wallet_profit_and_loss.rb index 0fffd3d..009b949 100644 --- a/lib/domeapi/polymarket/wallet_profit_and_loss.rb +++ b/lib/domeapi/polymarket/wallet_profit_and_loss.rb @@ -14,14 +14,12 @@ class Filter < Contract propertize(%i[wallet_address granularity start_time end_time]) validation do - # :nocov: params do required(:wallet_address).filled(:string) required(:granularity).filled(:string, included_in?: %w[day week month year all]) optional(:start_time).maybe(:integer) optional(:end_time).maybe(:integer) end - # :nocov: end end end diff --git a/lib/domeapi/types.rb b/lib/domeapi/types.rb new file mode 100644 index 0000000..ff1ea6b --- /dev/null +++ b/lib/domeapi/types.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'dry-types' + +module Rubyists + module Domeapi + # Custom types for Domeapi + module Types + include Dry.Types() + + # Polymarket slugs are typically kebab-case strings + PolymarketMarketSlug = String.constrained(format: /\A[a-z0-9]+(?:-[a-z0-9]+)+\z/) + + # Kalshi event tickers are typically uppercase alphanumeric strings, potentially with dashes + KalshiEventTicker = String.constrained(format: /\A[A-Z0-9]+(?:-[A-Z0-9]+)+\z/) + end + end +end diff --git a/test/domeapi/client_test.rb b/test/domeapi/client_test.rb index a783e1b..a299e57 100644 --- a/test/domeapi/client_test.rb +++ b/test/domeapi/client_test.rb @@ -32,4 +32,8 @@ it 'returns a polymarket client' do _(client.polymarket).must_be_instance_of Rubyists::Domeapi::Polymarket::Client end + + it 'returns a matching_markets client' do + _(client.matching_markets).must_be_instance_of Rubyists::Domeapi::MatchingMarkets + end end diff --git a/test/domeapi/matching_markets_test.rb b/test/domeapi/matching_markets_test.rb new file mode 100644 index 0000000..641eb3f --- /dev/null +++ b/test/domeapi/matching_markets_test.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require_relative '../helper' + +describe Rubyists::Domeapi::MatchingMarkets do + let(:client) { Rubyists::Domeapi::Client.new } + let(:matching_markets) { client.matching_markets } + + before do + Rubyists::Domeapi.configure do |config| + config.api_key = 'test_api_key' + end + end + + describe 'Sports' do + it 'lists matching markets for sports' do + stub_request(:get, 'https://api.domeapi.io/v1/matching-markets/sports') + .with(query: { polymarket_market_slug: 'valid-slug' }) + .to_return(status: 200, body: '[]') + + response = matching_markets.sports(polymarket_market_slug: 'valid-slug') + + _(response).must_equal [] + end + + it 'lists matching markets for sports with Filter object' do + stub_request(:get, 'https://api.domeapi.io/v1/matching-markets/sports') + .with(query: { polymarket_market_slug: 'valid-slug' }) + .to_return(status: 200, body: '[]') + + filter = Rubyists::Domeapi::MatchingMarkets::Filter.new( + Rubyists::Domeapi::MatchingMarkets::Filter::Properties.new(polymarket_market_slug: 'valid-slug') + ) + response = matching_markets.sports(filter) + + _(response).must_equal [] + end + + it 'validates filter' do + error = _ { matching_markets.sports({}) }.must_raise ArgumentError + _(error.message).must_include 'Either polymarket_market_slug or kalshi_event_ticker must be provided' + end + end + + describe 'Sports by Date' do + it 'lists matching markets for sport and date' do + stub_request(:get, 'https://api.domeapi.io/v1/matching-markets/sports/nfl') + .with(query: { date: '2023-01-01' }) + .to_return(status: 200, body: '[]') + + response = matching_markets.sports_by_date(sport: 'nfl', date: '2023-01-01') + + _(response).must_equal [] + end + + it 'accepts Date object' do + stub_request(:get, 'https://api.domeapi.io/v1/matching-markets/sports/nfl') + .with(query: { date: '2023-01-01' }) + .to_return(status: 200, body: '[]') + + require 'date' + response = matching_markets.sports_by_date(sport: 'nfl', date: Date.parse('2023-01-01')) + + _(response).must_equal [] + end + + it 'accepts SportsFilter object' do + stub_request(:get, 'https://api.domeapi.io/v1/matching-markets/sports/nfl') + .with(query: { date: '2023-01-01' }) + .to_return(status: 200, body: '[]') + + filter = Rubyists::Domeapi::MatchingMarkets::SportsFilter.new( + Rubyists::Domeapi::MatchingMarkets::SportsFilter::Properties.new(sport: 'nfl', date: '2023-01-01') + ) + response = matching_markets.sports_by_date(filter) + + _(response).must_equal [] + end + + it 'validates sport' do + error = _ { matching_markets.sports_by_date(sport: 'invalid', date: '2023-01-01') }.must_raise ArgumentError + _(error.message).must_include 'Sport must be one of: nfl, mlb, cfb, nba, nhl, cbb' + end + + it 'validates date format' do + error = _ { matching_markets.sports_by_date(sport: 'nfl', date: 'invalid') }.must_raise ArgumentError + _(error.message).must_include 'Date is in invalid format' + end + end + + describe 'Dynamic methods' do + it 'defines methods for each sport' do + stub_request(:get, 'https://api.domeapi.io/v1/matching-markets/sports/nfl') + .with(query: { date: '2023-01-01' }) + .to_return(status: 200, body: '[]') + + response = matching_markets.nfl_on('2023-01-01') + + _(response).must_equal [] + end + + it 'defines class methods for each sport' do + stub_request(:get, 'https://api.domeapi.io/v1/matching-markets/sports/nfl') + .with(query: { date: '2023-01-01' }) + .to_return(status: 200, body: '[]') + + response = Rubyists::Domeapi::MatchingMarkets.nfl_on('2023-01-01') + + _(response).must_equal [] + end + end +end