diff --git a/.github/workflows/standardrb.yaml b/.github/workflows/standardrb.yaml new file mode 100644 index 0000000..6003707 --- /dev/null +++ b/.github/workflows/standardrb.yaml @@ -0,0 +1,13 @@ +name: StandardRB + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: StandardRB Linter + uses: standardrb/standard-ruby-action@v0.0.5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.ruby-version b/.ruby-version index 460b6fd..be94e6f 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.5 \ No newline at end of file +3.2.2 diff --git a/Gemfile b/Gemfile index 79272f4..8407a4a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,11 @@ -source 'https://rubygems.org' +source "https://rubygems.org" # Specify your gem's dependencies in pager_duty-connection.gemspec gemspec # for tests & running examples group :development do - gem 'dotenv' - gem 'pry' + gem "dotenv" + gem "pry" + gem "standardrb" end diff --git a/README.md b/README.md index cc9b4c2..0d8114f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,9 @@ And this is what it doesn't do: Add this line to your application's Gemfile: - gem 'pager_duty-connection' +```ruby +gem "pager_duty-connection" +``` And then execute: @@ -54,22 +56,22 @@ pagerduty = PagerDuty::Connection.new(token) pagerduty = PagerDuty::Connection.new(token, token_type: :Bearer) # setup to use a custom domain -pagerduty = PagerDuty::Connection.new(token, token_type: :Bearer, url: 'https://custom.domain.com') +pagerduty = PagerDuty::Connection.new(token, token_type: :Bearer, url: "https://custom.domain.com") # 4 main methods: `get`, `post`, `put`, and `delete`: -response = pagerduty.get('some/relative/path', params) -response = pagerduty.post('some/relative/path', params) -response = pagerduty.delete('some/relative/path', params) -response = pagerduty.put('some/relative/path', params) +response = pagerduty.get("some/relative/path", params) +response = pagerduty.post("some/relative/path", params) +response = pagerduty.delete("some/relative/path", params) +response = pagerduty.put("some/relative/path", params) # use something like irb or pry to poke around the responses # the contents will vary a bit between call, ie: -response = pagerduty.get('incidents') +response = pagerduty.get("incidents") response.incidents # an array of incidents -response = pagerduty.get('incidents/YYZ') +response = pagerduty.get("incidents/YYZ") response # the hash/object that represents the array ``` @@ -102,14 +104,14 @@ In general, you can get/put/post/delete a path, with some attributes. Use the [R If you are working in Rails, and using only a single PagerDuty account, you'll probably want an initializer: ```ruby -$pagerduty = PagerDuty::Connection.new('your-token') +$pagerduty = PagerDuty::Connection.new("your-token") ``` And if you are using [dotenv](https://github.com/bkeepers/dotenv), you can use environment variables, and stash them in .env: ```ruby -account = ENV['PAGERDUTY_ACCOUNT'] || raise("Missing ENV['PAGERDUTY_ACCOUNT'], add to .env") -token = ENV['PAGERDUTY_TOKEN'] || raise("Missing ENV['PAGERDUTY_TOKEN'], add to .env.#{Rails.env}") +account = ENV["PAGERDUTY_ACCOUNT"] || raise("Missing ENV['PAGERDUTY_ACCOUNT'], add to .env") +token = ENV["PAGERDUTY_TOKEN"] || raise("Missing ENV['PAGERDUTY_TOKEN'], add to .env.#{Rails.env}") $pagerduty = PagerDuty::Connection.new(account, token) ``` diff --git a/examples/find-users.rb b/examples/find-users.rb index aeff403..e69f93a 100755 --- a/examples/find-users.rb +++ b/examples/find-users.rb @@ -1,15 +1,15 @@ #!/usr/bin/env ruby -require 'dotenv' -Dotenv.load ".env.development", '.env' +require "dotenv" +Dotenv.load ".env.development", ".env" -token = ENV['PAGERDUTY_TOKEN'] || raise("Missing ENV['PAGERDUTY_TOKEN'], add to .env.development") +token = ENV["PAGERDUTY_TOKEN"] || raise("Missing ENV['PAGERDUTY_TOKEN'], add to .env.development") -require 'pager_duty/connection' -$pagerduty = PagerDuty::Connection.new(token) +require "pager_duty/connection" +pagerduty = PagerDuty::Connection.new(token) # https://v2.developer.pagerduty.com/v2/page/api-reference#!/Users/get_users -response = $pagerduty.get('users') -response['users'].each do |user| - puts "#{user['name']}: #{user['email']}" +response = pagerduty.get("users") +response["users"].each do |user| + puts "#{user["name"]}: #{user["email"]}" end diff --git a/examples/shifts-with-incidents-and-log-entries.rb b/examples/shifts-with-incidents-and-log-entries.rb index e702b78..60a73c9 100755 --- a/examples/shifts-with-incidents-and-log-entries.rb +++ b/examples/shifts-with-incidents-and-log-entries.rb @@ -1,46 +1,46 @@ #!/usr/bin/env ruby -require 'dotenv' -Dotenv.load ".env.development", '.env' +require "dotenv" +Dotenv.load ".env.development", ".env" -token = ENV['PAGERDUTY_TOKEN'] || raise("Missing ENV['PAGERDUTY_TOKEN'], add to .env.development") +token = ENV["PAGERDUTY_TOKEN"] || raise("Missing ENV['PAGERDUTY_TOKEN'], add to .env.development") -require 'pager_duty/connection' -$pagerduty = PagerDuty::Connection.new(token) +require "pager_duty/connection" +pagerduty = PagerDuty::Connection.new(token) -schedule_id = ENV['PAGERDUTY_SCHEDULE_ID'] || raise("Missing ENV['PAGERDUTY_SCHEDULE_ID'], add to .env.development") +schedule_id = ENV["PAGERDUTY_SCHEDULE_ID"] || raise("Missing ENV['PAGERDUTY_SCHEDULE_ID'], add to .env.development") # pull down schedule entires for XXX schedule in the last day (ie who has been on call, and when time_since = 1.day.ago time_until = Time.now # https://v2.developer.pagerduty.com/v2/page/api-reference#!/On-Calls/get_oncalls -response = $pagerduty.get("oncalls", query_params: { since: time_since, until: time_until, schedule_ids: [schedule_id] }) +response = pagerduty.get("oncalls", query_params: {since: time_since, until: time_until, schedule_ids: [schedule_id]}) -entries = response['oncalls'] +entries = response["oncalls"] entries.each do |entry| - puts "#{entry['start']} - #{entry['end']}: #{entry['user']['summary']}" + puts "#{entry["start"]} - #{entry["end"]}: #{entry["user"]["summary"]}" # find incidents during that shift # https://v2.developer.pagerduty.com/v2/page/api-reference#!/Incidents/get_incidents - response = $pagerduty.get('incidents', query_params: { since: entry['start'], until: entry['end'], user_ids: [entry['user']['id']] }) + response = pagerduty.get("incidents", query_params: {since: entry["start"], until: entry["end"], user_ids: [entry["user"]["id"]]}) - response['incidents'].each do |incident| + response["incidents"].each do |incident| puts "\t#{incident.id}" # find log entries (acknowledged, notifications, etc) for incident: # https://v2.developer.pagerduty.com/v2/page/api-reference#!/Incidents/get_incidents_id_log_entries - response = $pagerduty.get("incidents/#{incident.id}/log_entries") + response = pagerduty.get("incidents/#{incident.id}/log_entries") # select just the notes - notes = response['log_entries'].select do |log_entry| - log_entry['channel'] && log_entry['channel']['type'] == 'note' + notes = response["log_entries"].select do |log_entry| + log_entry["channel"] && log_entry["channel"]["type"] == "note" end # and print them out: notes.each do |log_entry| - puts "\t\t#{log_entry['channel']['summary']}" + puts "\t\t#{log_entry["channel"]["summary"]}" end end end diff --git a/lib/pager_duty.rb b/lib/pager_duty.rb index 288caff..da09760 100644 --- a/lib/pager_duty.rb +++ b/lib/pager_duty.rb @@ -1,6 +1,5 @@ -require 'pager_duty/connection/version' +require "pager_duty/connection/version" module PagerDuty - autoload :Connection, 'pager_duty/connection' + autoload :Connection, "pager_duty/connection" end - diff --git a/lib/pager_duty/connection.rb b/lib/pager_duty/connection.rb index bc72149..f953095 100644 --- a/lib/pager_duty/connection.rb +++ b/lib/pager_duty/connection.rb @@ -1,23 +1,46 @@ -require 'faraday' -require 'active_support' -require 'active_support/core_ext' -require 'active_support/time_with_zone' +require "faraday" +require "hashie" +require "active_support" +require "active_support/core_ext" +require "active_support/time_with_zone" module PagerDuty - class Connection attr_accessor :connection API_VERSION = 2 API_PREFIX = "https://api.pagerduty.com/" - class FileNotFoundError < RuntimeError - end + class FileNotFoundError < RuntimeError; end + + class ApiError < RuntimeError; end + + class RateLimitError < RuntimeError; end + + class UnauthorizedError < RuntimeError; end + + class ForbiddenError < RuntimeError; end - class ApiError < RuntimeError + class RaiseUnauthorizedOn401 < Faraday::Middleware + def call(env) + response = @app.call(env) + if response.status == 401 + raise PagerDuty::Connection::UnauthorizedError, response.env[:url].to_s + else + response + end + end end - class RateLimitError < RuntimeError + class RaiseForbiddenOn403 < Faraday::Middleware + def call(env) + response = @app.call(env) + if response.status == 403 + raise PagerDuty::Connection::ForbiddenError, response.env[:url].to_s + else + response + end + end end class RaiseFileNotFoundOn404 < Faraday::Middleware @@ -34,18 +57,21 @@ def call(env) class RaiseApiErrorOnNon200 < Faraday::Middleware def call(env) response = @app.call env - unless [200, 201, 204].include?(response.status) + if [200, 201, 204].include?(response.status) + response + else url = response.env[:url].to_s message = "Got HTTP #{response.status}: #{response.reason_phrase}\nFrom #{url}" - if error = response.body - # TODO May Need to check error.errors too - message += "\n#{JSON.parse(error)}" + if (error = response.body) + begin + # TODO May Need to check error.errors too + message += "\n#{JSON.parse(error)}" + rescue JSON::ParserError + message += "\n#{error}" + end end - raise ApiError, message - else - response end end end @@ -64,11 +90,12 @@ def call(env) class ConvertTimesParametersToISO8601 < Faraday::Middleware TIME_KEYS = [:since, :until] def call(env) - body = env[:body] - TIME_KEYS.each do |key| - if body.has_key?(key) - body[key] = body[key].iso8601 if body[key].respond_to?(:iso8601) + unless body.nil? + TIME_KEYS.each do |key| + if body.has_key?(key) + body[key] = body[key].iso8601 if body[key].respond_to?(:iso8601) + end end end @@ -77,7 +104,7 @@ def call(env) end class ParseTimeStrings < Faraday::Middleware - TIME_KEYS = %w( + TIME_KEYS = %w[ at created_at created_on @@ -88,9 +115,9 @@ class ParseTimeStrings < Faraday::Middleware start started_at start_time - ) + ] - OBJECT_KEYS = %w( + OBJECT_KEYS = %w[ alert entry incident @@ -99,22 +126,18 @@ class ParseTimeStrings < Faraday::Middleware note override service - ) + ] - NESTED_COLLECTION_KEYS = %w( + NESTED_COLLECTION_KEYS = %w[ acknowledgers assigned_to pending_actions - ) + ] def on_complete(env) - if env.body - env.body = parse(env.body) - end + parse(env[:body]) end - private - def parse(body) if body.respond_to?(:empty?) && body.empty? return body @@ -151,7 +174,7 @@ def parse_collection_times(collection) end def parse_object_times(object) - time = Time.zone ? Time.zone : Time + time = Time.zone || Time TIME_KEYS.each do |key| if object.has_key?(key) && object[key].present? @@ -161,17 +184,37 @@ def parse_object_times(object) end end + class Mashify < Faraday::Middleware + def on_complete(env) + env[:body] = parse(env[:body]) + end + + def parse(body) + case body + when Hash + ::Hashie::Mash.new(body) + when Array + body.map { |item| parse(item) } + else + body + end + end + end + def initialize(token, token_type: :Token, url: API_PREFIX, debug: false) @connection = Faraday.new do |conn| conn.url_prefix = url case token_type when :Token - conn.request :authorization, "Token", "token=#{token}" + if faraday_v1? + conn.request :token_auth, token + else + conn.request :authorization, "Token", token + end when :Bearer conn.request :authorization, "Bearer", token - else - raise ArgumentError, "invalid token_type: #{token_type.inspect}" + else raise ArgumentError, "invalid token_type: #{token_type.inspect}" end conn.use ConvertTimesParametersToISO8601 @@ -181,8 +224,9 @@ def initialize(token, token_type: :Token, url: API_PREFIX, debug: false) conn.headers[:accept] = "application/vnd.pagerduty+json;version=#{API_VERSION}" conn.use ParseTimeStrings + conn.use Mashify conn.response :json - conn.response :logger, ::Logger.new(STDOUT), bodies: true if debug + conn.response :logger, ::Logger.new($stdout), bodies: true if debug # Because Faraday::Middleware executes in reverse order of # calls to conn.use, status code error handling goes at the @@ -190,12 +234,20 @@ def initialize(token, token_type: :Token, url: API_PREFIX, debug: false) conn.use RaiseApiErrorOnNon200 conn.use RaiseFileNotFoundOn404 conn.use RaiseRateLimitOn429 + conn.use RaiseForbiddenOn403 + conn.use RaiseUnauthorizedOn401 - conn.adapter Faraday.default_adapter + conn.adapter Faraday.default_adapter end end def get(path, request = {}) + # The run_request() method body argument defaults to {}, which is incorrect for GET requests + # https://github.com/technicalpickles/pager_duty-connection/issues/56 + # NOTE: PagerDuty support discourages GET requests with bodies, but not throwing an ArgumentError to prevent breaking + # corner-case implementations. + request[:body] = nil if !request[:body] + # paginate anything being 'get'ed, because the offset/limit isn't intuitive request[:query_params] = {} if !request[:query_params] page = request[:query_params].fetch(:page, 1).to_i @@ -203,6 +255,7 @@ def get(path, request = {}) offset = (page - 1) * limit query_params = request[:query_params].merge(offset: offset, limit: limit) + query_params.delete(:page) run_request(:get, path, **request.merge(query_params: query_params)) end @@ -221,8 +274,16 @@ def delete(path, request = {}) private + def faraday_v1? + faraday_version < Gem::Version.new("2") + end + + def faraday_version + @faraday_version ||= Gem.loaded_specs["faraday"].version + end + def run_request(method, path, body: {}, headers: {}, query_params: {}) - path = path.gsub(/^\//, '') # strip leading slash, to make sure relative things happen on the connection + path = path.gsub(/^\//, "") # strip leading slash, to make sure relative things happen on the connection connection.params = query_params response = connection.run_request(method, path, body, headers) diff --git a/lib/pager_duty/connection/version.rb b/lib/pager_duty/connection/version.rb index 8420552..659f274 100644 --- a/lib/pager_duty/connection/version.rb +++ b/lib/pager_duty/connection/version.rb @@ -1,5 +1,5 @@ module PagerDuty class Connection - VERSION = "2.1.0" + VERSION = "3.0.0" end end diff --git a/pager_duty-connection.gemspec b/pager_duty-connection.gemspec index a460e84..533a6ad 100644 --- a/pager_duty-connection.gemspec +++ b/pager_duty-connection.gemspec @@ -1,24 +1,22 @@ -# -*- encoding: utf-8 -*- -lib = File.expand_path('../lib', __FILE__) +lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'pager_duty/connection/version' +require "pager_duty/connection/version" Gem::Specification.new do |gem| - gem.name = "pager_duty-connection" - gem.version = PagerDuty::Connection::VERSION - gem.authors = ["Josh Nichols"] - gem.email = ["josh@technicalpickles.com"] - gem.description = %q{Ruby API wrapper for the PagerDuty REST API} - gem.summary = %q{Written with the power of faraday, pager_duty-connection tries to be a simple and usable Ruby API wrapper for the PagerDuty REST API} - gem.homepage = "http://github.com/technicalpickles/pager_duty-connection" + gem.name = "pager_duty-connection" + gem.version = PagerDuty::Connection::VERSION + gem.authors = ["Josh Nichols"] + gem.email = ["josh@technicalpickles.com"] + gem.description = "Ruby API wrapper for the PagerDuty REST API" + gem.summary = "Written with the power of faraday, pager_duty-connection tries to be a simple and usable Ruby API wrapper for the PagerDuty REST API" + gem.homepage = "http://github.com/technicalpickles/pager_duty-connection" - gem.files = `git ls-files`.split($/) - gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } - gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) + gem.files = `git ls-files`.split($/) + gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.require_paths = ["lib"] - gem.add_dependency "faraday", "~> 2.0" - gem.add_dependency "activesupport", ">= 3.2", "< 8.0" + gem.add_dependency "faraday", ">= 1.10", "< 3" + gem.add_dependency "activesupport", ">= 3.2", "<= 9.0" gem.add_development_dependency "rake" end