Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .standard.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# For available configuration options, see:
# https://github.com/standardrb/standard
ruby_version: 3.1

ignore:
- "lib/paystack/resources/**"
5 changes: 0 additions & 5 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,3 @@ source "https://rubygems.org"
gemspec

gem "irb"
gem "rake", "~> 13.0"

gem "minitest", "~> 5.16"

gem "standard", "~> 1.3"
106 changes: 106 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
PATH
remote: .
specs:
paystack-ruby (0.1.0)

GEM
remote: https://rubygems.org/
specs:
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3)
bigdecimal (4.1.2)
crack (1.0.1)
bigdecimal
rexml
date (3.5.1)
erb (6.0.4)
hashdiff (1.2.1)
io-console (0.8.2)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
json (2.19.9)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
minitest (5.27.0)
parallel (2.1.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
psych (5.4.0)
date
stringio
public_suffix (7.0.5)
racc (1.8.1)
rainbow (3.1.1)
rake (13.4.2)
rdoc (7.2.0)
erb
psych (>= 4.0.0)
tsort
regexp_parser (2.12.0)
reline (0.6.3)
io-console (~> 0.5)
rexml (3.4.4)
rubocop (1.87.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (>= 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.1)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-performance (1.26.1)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (1.13.0)
standard (1.55.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.87.0)
standard-custom (~> 1.0.0)
standard-performance (~> 1.8)
standard-custom (1.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.50)
standard-performance (1.9.0)
lint_roller (~> 1.1)
rubocop-performance (~> 1.26.0)
stringio (3.2.0)
tsort (0.2.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
webmock (3.26.2)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)

PLATFORMS
arm64-darwin-24
ruby

DEPENDENCIES
irb
minitest (~> 5.16)
paystack-ruby!
rake (~> 13.0)
standard (~> 1.3)
webmock (~> 3.0)

BUNDLED WITH
2.6.9
17 changes: 17 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,21 @@ Minitest::TestTask.create

require "standard/rake"

namespace :openapi do
desc "Fetch latest Paystack OpenAPI spec from GitHub"
task :update do
require "net/http"
require "uri"
uri = URI("https://raw.githubusercontent.com/PaystackOSS/openapi/main/dist/paystack.yaml")
content = Net::HTTP.get(uri)
File.write("openapi/paystack.yaml", content)
puts "Updated openapi/paystack.yaml"
end
end

desc "Generate resource files from openapi/paystack.yaml"
task :generate do
ruby "generator/generate.rb"
end

task default: %i[test standard]
109 changes: 109 additions & 0 deletions generator/generate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "yaml"
require "erb"
require "fileutils"

SPEC_PATH = File.expand_path("../openapi/paystack.yaml", __dir__)
TEMPLATE_PATH = File.expand_path("templates/resource.erb", __dir__)
OUTPUT_DIR = File.expand_path("../lib/paystack/resources", __dir__)

RESOURCES = %w[Transaction Customer].freeze

def snake_case(str)
str
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
.downcase
end

# Strip the tag prefix from an operationId and convert to snake_case.
# e.g. "transaction_chargeAuthorization" -> "charge_authorization"
# "customer_riskAction" -> "risk_action"
def method_name(operation_id, tag)
without_prefix = operation_id.sub(/\A#{Regexp.escape(tag)}_/i, "")
snake_case(without_prefix)
end

# Build the method signature from path params and whether the op has a body or query.
def build_signature(path_params, http_method, has_body)
parts = path_params.map { |p| "#{snake_case(p)}:" }

if has_body
parts << "**params"
elsif %w[get delete].include?(http_method)
parts << "**params"
end

parts.join(", ")
end

# Build the transport path, interpolating path params as Ruby string interpolation.
def build_transport_path(path, path_params)
result = path.dup
path_params.each do |p|
result = result.gsub("{#{p}}", "\#{#{snake_case(p)}}")
end
result
end

# Build the trailing args for the transport call.
def build_transport_args(http_method, has_body, path_params)
uses_params = has_body || %w[get delete].include?(http_method)
return "" unless uses_params

if has_body
", body: params"
else
", query: params"
end
end

spec = YAML.load_file(SPEC_PATH, aliases: true)
template = ERB.new(File.read(TEMPLATE_PATH), trim_mode: "-")

RESOURCES.each do |tag|
operations = []

spec["paths"].each do |path, path_item|
# Path-level parameters (shared across methods on this path)
shared_params = (path_item["parameters"] || [])
.select { |p| p.is_a?(Hash) && p["in"] == "path" }
.map { |p| p["name"] }

path_item.each do |http_method, op|
next unless op.is_a?(Hash) && op["tags"]&.include?(tag)

op_path_params = shared_params + op.fetch("parameters", [])
.select { |p| p.is_a?(Hash) && p["in"] == "path" }
.map { |p| p["name"] }

has_body = op.key?("requestBody")
op_id = op["operationId"]
name = method_name(op_id, tag)

transport_path = build_transport_path(path, op_path_params)
uses_params = has_body || %w[get delete].include?(http_method)

operations << {
summary: op["summary"] || op_id,
http_method: http_method,
path: path,
method_name: name,
signature: build_signature(op_path_params, http_method, has_body),
transport_path: transport_path,
transport_args: build_transport_args(http_method, has_body, op_path_params),
uses_params: uses_params
}
end
end

class_name = tag
output = template.result(binding)

FileUtils.mkdir_p(OUTPUT_DIR)
out_path = File.join(OUTPUT_DIR, "#{snake_case(tag)}.rb")
File.write(out_path, output)
puts "Generated #{out_path} (#{operations.size} operations)"
end
19 changes: 19 additions & 0 deletions generator/templates/resource.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true
# Generated by generator/generate.rb — do not edit manually

module Paystack
module Resources
class <%= class_name %> < BaseResource
<% operations.each do |op| %>
# <%= op[:summary] %>
# <%= op[:http_method].upcase %> <%= op[:path] %>
def <%= op[:method_name] %>(<%= op[:signature] %>)
@transport.<%= op[:http_method] %>("<%= op[:transport_path] %>"<%= op[:transport_args] %>)
end
<% if op[:method_name] == "initialize" %>
public :initialize
<% end %>
<% end %>
end
end
end
13 changes: 13 additions & 0 deletions lib/paystack.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

require_relative "paystack/version"
require_relative "paystack/errors"
require_relative "paystack/response"
require_relative "paystack/transport"
require_relative "paystack/base_resource"
require_relative "paystack/resources/transaction"
require_relative "paystack/resources/customer"
require_relative "paystack/client"

module Paystack
end
13 changes: 13 additions & 0 deletions lib/paystack/base_resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module Paystack
class BaseResource
# Override .new so subclasses can use #initialize as an API method
# without conflicting with Ruby's constructor protocol.
def self.new(transport)
instance = allocate
instance.instance_variable_set(:@transport, transport)
instance
end
end
end
17 changes: 17 additions & 0 deletions lib/paystack/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Paystack
class Client
def initialize(api_key)
@transport = Transport.new(api_key)
end

def transaction
@transaction ||= Resources::Transaction.new(@transport)
end

def customer
@customer ||= Resources::Customer.new(@transport)
end
end
end
19 changes: 19 additions & 0 deletions lib/paystack/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module Paystack
class Error < StandardError
attr_reader :http_status, :data

def initialize(message = nil, http_status: nil, data: nil)
super(message)
@http_status = http_status
@data = data
end
end

AuthenticationError = Class.new(Error)
ValidationError = Class.new(Error)
NotFoundError = Class.new(Error)
RateLimitError = Class.new(Error)
ServerError = Class.new(Error)
end
Loading
Loading