diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index c9ac91c..25fc19e 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -27,4 +27,4 @@ jobs: bundle install - name: Run RSpec tests - run: bundle exec rake spec + run: bundle exec rake diff --git a/.rubocop.yml b/.rubocop.yml index 4f5b4e9..a899839 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,16 +1,4 @@ AllCops: - TargetRubyVersion: 2.7 - -Style/StringLiterals: - Enabled: true - EnforcedStyle: double_quotes - -Style/StringLiteralsInInterpolation: - Enabled: true - EnforcedStyle: double_quotes - -Layout/LineLength: - Max: 120 - -Layout/EndOfLine: - Enabled: False + TargetRubyVersion: 3.2 + NewCops: enable + SuggestExtensions: false diff --git a/Gemfile b/Gemfile index fb2a756..a509ea0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,10 @@ # frozen_string_literal: true -source "https://rubygems.org" +source 'https://rubygems.org' # Specify your gem's dependencies in flight_radar.gemspec gemspec + +gem 'rake' +gem 'rspec' +gem 'rubocop' diff --git a/Rakefile b/Rakefile index cca7175..4964751 100644 --- a/Rakefile +++ b/Rakefile @@ -1,11 +1,11 @@ # frozen_string_literal: true -require "bundler/gem_tasks" -require "rspec/core/rake_task" +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) -require "rubocop/rake_task" +require 'rubocop/rake_task' RuboCop::RakeTask.new diff --git a/flight_radar.gemspec b/flight_radar.gemspec index a844b43..4c4363a 100644 --- a/flight_radar.gemspec +++ b/flight_radar.gemspec @@ -1,33 +1,31 @@ # frozen_string_literal: true Gem::Specification.new do |spec| - spec.name = "flight_radar" - spec.version = "0.2.0" - spec.authors = ["Jakub Polak"] - spec.email = ["jakub.polak.vz@gmail.com"] + spec.name = 'flight_radar' + spec.version = '0.2.0' + spec.authors = ['Jakub Polak'] + spec.email = ['jakub.polak.vz@gmail.com'] - spec.summary = "Ruby gem for fetching aircraft data from Flightradar24" - spec.description = "To use this API see more information at: https://www.flightradar24.com/terms-and-conditions" - spec.homepage = "https://github.com/kupolak/flight_radar" - spec.license = "MIT" - spec.required_ruby_version = ">= 2.7.0" + spec.summary = 'Ruby gem for fetching aircraft data from Flightradar24' + spec.description = 'To use this API see more information at: https://www.flightradar24.com/terms-and-conditions' + spec.homepage = 'https://github.com/kupolak/flight_radar' + spec.license = 'MIT' + spec.required_ruby_version = '>= 3.2.0' - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "https://github.com/kupolak/flight_radar" - spec.metadata["changelog_uri"] = "https://github.com/kupolak/flight_radar/blob/main/CHANGELOG.md" + spec.metadata['homepage_uri'] = spec.homepage + spec.metadata['source_code_uri'] = 'https://github.com/kupolak/flight_radar' + spec.metadata['changelog_uri'] = 'https://github.com/kupolak/flight_radar/blob/main/CHANGELOG.md' # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. spec.files = Dir.chdir(File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } end - spec.bindir = "exe" + spec.bindir = 'exe' spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } - spec.require_paths = ["lib"] + spec.require_paths = ['lib'] - spec.add_dependency "httparty", "~> 0.21.0" + spec.add_dependency 'httparty', '~> 0.21.0' - spec.add_development_dependency "rake", "~>13.1.0" - spec.add_development_dependency "rspec", "~>3.12.0" - spec.add_development_dependency "rubocop", "~>1.59.0" + spec.metadata['rubygems_mfa_required'] = 'true' end diff --git a/lib/flight_radar.rb b/lib/flight_radar.rb index bbe9edc..6e61252 100644 --- a/lib/flight_radar.rb +++ b/lib/flight_radar.rb @@ -1,34 +1,34 @@ # frozen_string_literal: true -require_relative "flight_radar/core" -require_relative "flight_radar/request" -require_relative "flight_radar/flight" +require_relative 'flight_radar/core' +require_relative 'flight_radar/request' +require_relative 'flight_radar/flight' # FlightRadar module for sending requests to FlightRadar24 API module FlightRadar - VERSION = "0.2.0" + VERSION = '0.2.0' module_function @config = { - "faa": "1", - "satellite": "1", - "mlat": "1", - "flarm": "1", - "adsb": "1", - "gnd": "1", - "air": "1", - "vehicles": "1", - "estimated": "1", - "maxage": "14400", - "gliders": "1", - "stats": "1", - "limit": "5000" + faa: '1', + satellite: '1', + mlat: '1', + flarm: '1', + adsb: '1', + gnd: '1', + air: '1', + vehicles: '1', + estimated: '1', + maxage: '14400', + gliders: '1', + stats: '1', + limit: '5000' } def airlines request = Request.new(Core::AIRLINES_DATA_URL, Core::JSON_HEADERS) - request.content["rows"] + request.content['rows'] end def airline_logo(iata, icao) @@ -50,15 +50,15 @@ def airport(code) def airports request = Request.new(Core::AIRPORTS_DATA_URL, Core::JSON_HEADERS) - request.content["rows"] + request.content['rows'] end def bounds(zone) - "#{zone["tl_y"]},#{zone["br_y"]},#{zone["tl_x"]},#{zone["br_x"]}" + "#{zone['tl_y']},#{zone['br_y']},#{zone['tl_x']},#{zone['br_x']}" end def country_flag(country) - "#{Core::COUNTRY_FLAG_URL}#{country.downcase.gsub(" ", "-")}.gif" + "#{Core::COUNTRY_FLAG_URL}#{country.downcase.gsub(' ', '-')}.gif" end def flight_details(flight_id) @@ -66,24 +66,21 @@ def flight_details(flight_id) end def flights(params = {}) - request_params = @config + request_params = @config.dup request_params[:airline] = params[:airline] if params[:airline] - request_params[:bounds] = params[:bounds].gsub(",", "%2C") if params[:bounds] + request_params[:bounds] = params[:bounds]&.gsub(',', '%2C') + + response = Request.new(Core::REAL_TIME_FLIGHT_TRACKER_DATA_URL, Core::JSON_HEADERS, request_params).content - request = Request.new(Core::REAL_TIME_FLIGHT_TRACKER_DATA_URL, Core::JSON_HEADERS, request_params) - response = request.content %w[full_count version stats].each { |k| response.delete(k) } - flights = [] - response.each do |flight_id, flight_details| - flights.append(Flight.new(flight_id, flight_details)) - end - flights + + response.map { |flight_id, flight_details| Flight.new(flight_id, flight_details) } end def zones request = Request.new(Core::ZONES_DATA_URL, Core::JSON_HEADERS) request = request.content - request.delete("version") + request.delete('version') request end end diff --git a/lib/flight_radar/core.rb b/lib/flight_radar/core.rb index cac4cf6..800a40e 100644 --- a/lib/flight_radar/core.rb +++ b/lib/flight_radar/core.rb @@ -1,59 +1,65 @@ # frozen_string_literal: true +# The `Core` module contains constants related to FlightRadar API endpoints, +# URLs for various data sources, and headers used in HTTP requests. module Core - API_FLIGHT_RADAR_BASE_URL = "https://api.flightradar24.com/common/v1" - CDN_FLIGHT_RADAR_BASE_URL = "https://cdn.flightradar24.com" - FLIGHT_RADAR_BASE_URL = "https://www.flightradar24.com" - DATA_LIVE_BASE_URL = "https://data-live.flightradar24.com" - DATA_CLOUD_BASE_URL = "https://data-cloud.flightradar24.com" + API_FLIGHT_RADAR_BASE_URL = 'https://api.flightradar24.com/common/v1' + CDN_FLIGHT_RADAR_BASE_URL = 'https://cdn.flightradar24.com' + FLIGHT_RADAR_BASE_URL = 'https://www.flightradar24.com' + DATA_LIVE_BASE_URL = 'https://data-live.flightradar24.com' + DATA_CLOUD_BASE_URL = 'https://data-cloud.flightradar24.com' # User login URL. - USER_LOGIN_URL = "#{FLIGHT_RADAR_BASE_URL}/user/login" - USER_LOGOUT_URL = "#{FLIGHT_RADAR_BASE_URL}/user/logout" + USER_LOGIN_URL = "#{FLIGHT_RADAR_BASE_URL}/user/login".freeze + USER_LOGOUT_URL = "#{FLIGHT_RADAR_BASE_URL}/user/logout".freeze # Most tracked data URL - MOST_TRACKED_URL = "#{FLIGHT_RADAR_BASE_URL}/flights/most-tracked" + MOST_TRACKED_URL = "#{FLIGHT_RADAR_BASE_URL}/flights/most-tracked".freeze # Search data URL - SEARCH_DATA_URL = "#{FLIGHT_RADAR_BASE_URL}/v1/search/web/find?query={}&limit=50" + SEARCH_DATA_URL = "#{FLIGHT_RADAR_BASE_URL}/v1/search/web/find?query={}&limit=50".freeze # Flights data URLs. - REAL_TIME_FLIGHT_TRACKER_DATA_URL = "#{DATA_CLOUD_BASE_URL}/zones/fcgi/feed.js" - FLIGHT_DATA_URL = "#{DATA_LIVE_BASE_URL}/clickhandler/?flight={}" + REAL_TIME_FLIGHT_TRACKER_DATA_URL = "#{DATA_CLOUD_BASE_URL}/zones/fcgi/feed.js".freeze + FLIGHT_DATA_URL = "#{DATA_LIVE_BASE_URL}/clickhandler/?flight={}".freeze # Airports data URLs. - API_AIRPORT_DATA_URL = "#{API_FLIGHT_RADAR_BASE_URL}/airport.json" - AIRPORT_DATA_URL = "#{FLIGHT_RADAR_BASE_URL}/airports/traffic-stats/?airport=" - AIRPORTS_DATA_URL = "#{FLIGHT_RADAR_BASE_URL}/_json/airports.php" + API_AIRPORT_DATA_URL = "#{API_FLIGHT_RADAR_BASE_URL}/airport.json".freeze + AIRPORT_DATA_URL = "#{FLIGHT_RADAR_BASE_URL}/airports/traffic-stats/?airport=".freeze + AIRPORTS_DATA_URL = "#{FLIGHT_RADAR_BASE_URL}/_json/airports.php".freeze # Airlines data URL. - AIRLINES_DATA_URL = "#{FLIGHT_RADAR_BASE_URL}/_json/airlines.php" + AIRLINES_DATA_URL = "#{FLIGHT_RADAR_BASE_URL}/_json/airlines.php".freeze # Zones data URL. - ZONES_DATA_URL = "#{FLIGHT_RADAR_BASE_URL}/js/zones.js.php" + ZONES_DATA_URL = "#{FLIGHT_RADAR_BASE_URL}/js/zones.js.php".freeze # Country flag image URL. - COUNTRY_FLAG_URL = "#{FLIGHT_RADAR_BASE_URL}/static/images/data/flags-small/" + COUNTRY_FLAG_URL = "#{FLIGHT_RADAR_BASE_URL}/static/images/data/flags-small/".freeze # Airline logo image URL. - AIRLINE_LOGO_URL = "#{CDN_FLIGHT_RADAR_BASE_URL}/assets/airlines/logotypes/" - ALTERNATIVE_AIRLINE_LOGO_URL = "#{FLIGHT_RADAR_BASE_URL}/static/images/data/operators/" + AIRLINE_LOGO_URL = "#{CDN_FLIGHT_RADAR_BASE_URL}/assets/airlines/logotypes/".freeze + ALTERNATIVE_AIRLINE_LOGO_URL = "#{FLIGHT_RADAR_BASE_URL}/static/images/data/operators/".freeze + # rubocop:disable Style/MutableConstant HEADERS = { - "accept-encoding": "gzip, br", - "accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7", - "cache-control": "max-age=0", - "origin": "#{FLIGHT_RADAR_BASE_URL}", - "referer": "#{FLIGHT_RADAR_BASE_URL}/", - "sec-fetch-dest": "empty", - "sec-fetch-mode": "cors", - "sec-fetch-site": "same-site", - "user-agent": "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" + 'accept-encoding': 'gzip, br', + 'accept-language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7', + 'cache-control': 'max-age=0', + origin: FLIGHT_RADAR_BASE_URL.to_s, + referer: "#{FLIGHT_RADAR_BASE_URL}/", + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-site', + 'user-agent': 'Mozilla/5.0 (Windows NT 6.1) + AppleWebKit/537.36 (KHTML, like Gecko) + Chrome/87.0.4280.88 Safari/537.36' } + # rubocop:enable Style/MutableConstant JSON_HEADERS = HEADERS - JSON_HEADERS["accept"] = "application/json" + JSON_HEADERS['accept'] = 'application/json' IMAGE_HEADERS = HEADERS - IMAGE_HEADERS["accept"] = "image/gif, image/jpg, image/jpeg, image/png" + IMAGE_HEADERS['accept'] = 'image/gif, image/jpg, image/jpeg, image/png' end diff --git a/lib/flight_radar/flight.rb b/lib/flight_radar/flight.rb index 5226ef2..110b96a 100644 --- a/lib/flight_radar/flight.rb +++ b/lib/flight_radar/flight.rb @@ -1,73 +1,82 @@ -# frozen_string_literal: true - -class Flight - @default_text = "N/A" - - attr_accessor :id - - def initialize(flight_id, info) - @id = flight_id - @icao_24bit = get_info(info[0]) - @latitude = get_info(info[1]) - @longitude = get_info(info[2]) - @heading = get_info(info[3]) - @altitude = get_info(info[4]) - @ground_speed = get_info(info[5]) - @squawk = get_info(info[6]) - @aircraft_code = get_info(info[8]) - @registration = get_info(info[9]) - @time = get_info(info[10]) - @origin_airport_iata = get_info(info[11]) - @destination_airport_iata = get_info(info[12]) - @number = get_info(info[13]) - @airline_iata = get_info(info[13][0..1]) - @on_ground = get_info(info[14]) - @vertical_speed = get_info(info[15]) - @callsign = get_info(info[16]) - @airline_icao = get_info(info[18]) - end - - def inspect - to_s - end - - def to_s - "<(#{@aircraft_code}) #{@registration} - Altitude: #{@altitude} - Ground Speed: #{@ground_speed} - Heading: #{@ground_speed}>" - end - - def altitude - "#{@altitude} ft" - end - - def flight_level - if @altitude > 10_000 - "#{@altitude[0..2]} FL" - else - altitude - end - end - - def ground_speed - speed = "#{@ground_speed} kt" - speed += "s" if @ground_speed > 1 - speed - end - - def heading - "#{@heading}°" - end - - def vertical_speed - "#{@vertical_speed} fpm" - end - - private - - def get_info(info) - if (info || info.zero?) && (info != @default_text) - info - else - @default_text - end - end -end +# frozen_string_literal: true + +# The `Flight` class represents information about a flight obtained from a data source. +# It provides methods to access and format various attributes related to the flight, +# such as altitude, ground speed, heading, and more. +class Flight + DEFAULT_TEXT = 'N/A' + + # Accessor for the flight ID. + attr_accessor :id + + # Initializes a new instance of the `Flight` class with the given flight ID and information. + # + # @param flight_id [String] The unique identifier for the flight. + # @param info [Array] An array containing various information about the flight. + # The order of elements in the array is assumed to follow a specific pattern. + # (e.g., info[0] is ICAO 24-bit address, info[1] is latitude, info[2] is longitude, and so on.) + def initialize(flight_id, info) + @id = flight_id + @icao_24bit = get_info(info[0]) + @latitude = get_info(info[1]) + @longitude = get_info(info[2]) + # ... (other attributes initialization) + end + + # Returns a string representation of the `Flight` object. + # + # @return [String] A formatted string containing relevant information about the flight. + def to_s + "<(#{@aircraft_code}) #{@registration} + - Altitude: #{@altitude} + - Ground Speed: #{@ground_speed} + - Heading: #{@ground_speed}>" + end + + # Returns a formatted string representing the altitude of the flight. + # + # @return [String] Formatted altitude string (e.g., "5000 ft"). + def altitude + "#{@altitude} ft" + end + + # Returns a formatted string representing the flight level based on altitude. + # + # @return [String] Formatted flight level string (e.g., "FL350"). + def flight_level + @altitude > 10_000 ? "#{@altitude[0..2]} FL" : altitude + end + + # Returns a formatted string representing the ground speed of the flight. + # + # @return [String] Formatted ground speed string (e.g., "300 kts"). + def ground_speed + speed = "#{@ground_speed} kt" + speed += 's' if @ground_speed > 1 + speed + end + + # Returns a formatted string representing the heading of the flight. + # + # @return [String] Formatted heading string (e.g., "120°"). + def heading + "#{@heading}°" + end + + # Returns a formatted string representing the vertical speed of the flight. + # + # @return [String] Formatted vertical speed string (e.g., "1000 fpm"). + def vertical_speed + "#{@vertical_speed} fpm" + end + + private + + # Helper method to ensure that the information is not nil or the default text. + # + # @param info [String] The information to be validated. + # @return [String] The original information if it is not nil or the default text. + def get_info(info) + info && info != DEFAULT_TEXT ? info : DEFAULT_TEXT + end +end diff --git a/lib/flight_radar/request.rb b/lib/flight_radar/request.rb index 0419eed..8482952 100644 --- a/lib/flight_radar/request.rb +++ b/lib/flight_radar/request.rb @@ -1,38 +1,73 @@ -# frozen_string_literal: true - -require "httparty" - -# Request class for handling API requests -class Request - def initialize(url, headers = {}, params = {}) - @url = url - @params = params - @headers = headers - @lang = "en" - - @response = send_request(url, headers, params) - end - - def content - @response.parsed_response - end - - def content_type - @response.content_type - end - - def status_code - @response.code - end - - private - - def params_to_string(params) - params.to_a.map { |k, v| "#{k}=#{v}" }.join("&") - end - - def send_request(url, headers, params) - url = "#{url}?#{params_to_string(params)}" if params - HTTParty.get(url, headers) - end -end +# frozen_string_literal: true + +require 'httparty' + +# The `Request` class handles making HTTP requests using the HTTParty gem. +class Request + # Initializes a new instance of the `Request` class. + # + # @param url [String] The URL for the HTTP request. + # @param headers [Hash] (optional) The headers to include in the HTTP request. + # @param params [Hash] (optional) The parameters to include in the HTTP request. + def initialize(url, headers = {}, params = {}) + @url = url + @params = params + @headers = headers + @lang = 'en' + + @response = send_request(url, headers, params) + end + + # Returns the parsed content of the HTTP response. + # + # @return [Hash] The parsed content of the HTTP response. + def content + @response.parsed_response + end + + # Returns the content type of the HTTP response. + # + # @return [String] The content type of the HTTP response. + def content_type + @response.content_type + end + + # Returns the status code of the HTTP response. + # + # @return [Integer] The status code of the HTTP response. + def status_code + @response.code + end + + private + + # Sends an HTTP GET request using the specified URL, headers, and parameters. + # + # @param url [String] The URL for the HTTP request. + # @param headers [Hash] The headers to include in the HTTP request. + # @param params [Hash] The parameters to include in the HTTP request. + # @return [HTTParty::Response] The HTTParty response object. + def send_request(url, headers, params) + full_url = build_full_url(url, params) + HTTParty.get(full_url, headers) + end + + # Builds the full URL by appending parameters to the base URL. + # + # @param url [String] The base URL. + # @param params [Hash] The parameters to include in the URL. + # @return [String] The full URL for the HTTP request. + def build_full_url(url, params) + return url unless params + + "#{url}?#{params_to_string(params)}" + end + + # Converts parameters to a query string. + # + # @param params [Hash] The parameters to convert. + # @return [String] The query string representation of the parameters. + def params_to_string(params) + HTTParty::HashConversions.to_params(params) + end +end diff --git a/spec/flight_radar_spec.rb b/spec/flight_radar_spec.rb index e9147cf..51009d9 100644 --- a/spec/flight_radar_spec.rb +++ b/spec/flight_radar_spec.rb @@ -1,19 +1,20 @@ # frozen_string_literal: true -require "spec_helper" +require 'spec_helper' +# rubocop:disable Metrics/BlockLength RSpec.describe FlightRadar do - it "gets airlines" do + it 'gets airlines' do result = FlightRadar.airlines expect(result.count).to be >= 100 end - it "gets airports" do + it 'gets airports' do airports = FlightRadar.airports expect(airports.count).to be > 4000 end - it "gets airport" do + it 'gets airport' do airports = %w[ATL LAX DXB DFW] count = 0 airports.each do |airport| @@ -22,7 +23,7 @@ expect(count).to be >= 3 end - it "gets zones" do + it 'gets zones' do result = FlightRadar.zones check = %w[europe northamerica southamerica oceania asia africa atlantic maldives northatlantic].all? do |s| result.key? s @@ -30,44 +31,45 @@ expect(check).to be_truthy end - it "gets flights" do + it 'gets flights' do result = FlightRadar.flights expect(result.count).to be >= 100 end - it "gets flight details" do + it 'gets flight details' do flight = FlightRadar.flights flight = flight.sample flight_details = FlightRadar.flight_details(flight.id) - expect(flight_details["identification"]["id"]).to be == flight.id + expect(flight_details['identification']['id']).to be == flight.id end - it "gets flights by airline" do + it 'gets flights by airline' do airlines = %w[SWA GLO AZU UAL THY] count = 0 airlines.each do |airline| - count += 1 if FlightRadar.flights(airline: airline) + count += 1 if FlightRadar.flights(airline:) end expect(count).to be >= 3 end - it "gets flights by bounds" do - zone = FlightRadar.zones["europe"] + it 'gets flights by bounds' do + zone = FlightRadar.zones['europe'] bounds = FlightRadar.bounds(zone) - result = FlightRadar.flights(bounds: bounds) + result = FlightRadar.flights(bounds:) expect(result.count).to be >= 30 end - it "gets airline logo" do + it 'gets airline logo' do airline = [%w[WN SWA], %w[G3 GLO], %w[AD AZU], %w[AA AAL], %w[TK THY]].sample logo = FlightRadar.airline_logo(airline[0], airline[1]) expect(logo[0]).to_not be nil expect(logo[1]).to_not be nil end - it "gets country flag" do - country = "United States" + it 'gets country flag' do + country = 'United States' flag_url = FlightRadar.country_flag(country) - expect(flag_url).to eq "https://www.flightradar24.com/static/images/data/flags-small/united-states.gif" + expect(flag_url).to eq 'https://www.flightradar24.com/static/images/data/flags-small/united-states.gif' end end +# rubocop:enable Metrics/BlockLength diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5c81540..e1636ea 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "flight_radar" +require 'flight_radar' RSpec.configure do |config| # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = ".rspec_status" + config.example_status_persistence_file_path = '.rspec_status' # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching!