Skip to content

Commit ac72a3d

Browse files
add open api spec to SDK
1 parent b863741 commit ac72a3d

24 files changed

Lines changed: 20009 additions & 33 deletions

.standard.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
# For available configuration options, see:
22
# https://github.com/standardrb/standard
33
ruby_version: 3.1
4+
5+
ignore:
6+
- "lib/paystack/resources/**"

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ gem "irb"
99
gem "rake", "~> 13.0"
1010

1111
gem "minitest", "~> 5.16"
12+
gem "webmock", "~> 3.0"
1213

1314
gem "standard", "~> 1.3"

Gemfile.lock

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
PATH
2+
remote: .
3+
specs:
4+
paystack-ruby (0.1.0)
5+
6+
GEM
7+
remote: https://rubygems.org/
8+
specs:
9+
addressable (2.9.0)
10+
public_suffix (>= 2.0.2, < 8.0)
11+
ast (2.4.3)
12+
bigdecimal (4.1.2)
13+
crack (1.0.1)
14+
bigdecimal
15+
rexml
16+
date (3.5.1)
17+
erb (6.0.4)
18+
hashdiff (1.2.1)
19+
io-console (0.8.2)
20+
irb (1.18.0)
21+
pp (>= 0.6.0)
22+
prism (>= 1.3.0)
23+
rdoc (>= 4.0.0)
24+
reline (>= 0.4.2)
25+
json (2.19.9)
26+
language_server-protocol (3.17.0.5)
27+
lint_roller (1.1.0)
28+
minitest (5.27.0)
29+
parallel (2.1.0)
30+
parser (3.3.11.1)
31+
ast (~> 2.4.1)
32+
racc
33+
pp (0.6.3)
34+
prettyprint
35+
prettyprint (0.2.0)
36+
prism (1.9.0)
37+
psych (5.4.0)
38+
date
39+
stringio
40+
public_suffix (7.0.5)
41+
racc (1.8.1)
42+
rainbow (3.1.1)
43+
rake (13.4.2)
44+
rdoc (7.2.0)
45+
erb
46+
psych (>= 4.0.0)
47+
tsort
48+
regexp_parser (2.12.0)
49+
reline (0.6.3)
50+
io-console (~> 0.5)
51+
rexml (3.4.4)
52+
rubocop (1.87.0)
53+
json (~> 2.3)
54+
language_server-protocol (~> 3.17.0.2)
55+
lint_roller (~> 1.1.0)
56+
parallel (>= 1.10)
57+
parser (>= 3.3.0.2)
58+
rainbow (>= 2.2.2, < 4.0)
59+
regexp_parser (>= 2.9.3, < 3.0)
60+
rubocop-ast (>= 1.49.0, < 2.0)
61+
ruby-progressbar (~> 1.7)
62+
unicode-display_width (>= 2.4.0, < 4.0)
63+
rubocop-ast (1.49.1)
64+
parser (>= 3.3.7.2)
65+
prism (~> 1.7)
66+
rubocop-performance (1.26.1)
67+
lint_roller (~> 1.1)
68+
rubocop (>= 1.75.0, < 2.0)
69+
rubocop-ast (>= 1.47.1, < 2.0)
70+
ruby-progressbar (1.13.0)
71+
standard (1.55.0)
72+
language_server-protocol (~> 3.17.0.2)
73+
lint_roller (~> 1.0)
74+
rubocop (~> 1.87.0)
75+
standard-custom (~> 1.0.0)
76+
standard-performance (~> 1.8)
77+
standard-custom (1.0.2)
78+
lint_roller (~> 1.0)
79+
rubocop (~> 1.50)
80+
standard-performance (1.9.0)
81+
lint_roller (~> 1.1)
82+
rubocop-performance (~> 1.26.0)
83+
stringio (3.2.0)
84+
tsort (0.2.0)
85+
unicode-display_width (3.2.0)
86+
unicode-emoji (~> 4.1)
87+
unicode-emoji (4.2.0)
88+
webmock (3.26.2)
89+
addressable (>= 2.8.0)
90+
crack (>= 0.3.2)
91+
hashdiff (>= 0.4.0, < 2.0.0)
92+
93+
PLATFORMS
94+
arm64-darwin-24
95+
ruby
96+
97+
DEPENDENCIES
98+
irb
99+
minitest (~> 5.16)
100+
paystack-ruby!
101+
rake (~> 13.0)
102+
standard (~> 1.3)
103+
webmock (~> 3.0)
104+
105+
BUNDLED WITH
106+
2.6.9

Rakefile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,21 @@ Minitest::TestTask.create
77

88
require "standard/rake"
99

10+
namespace :openapi do
11+
desc "Fetch latest Paystack OpenAPI spec from GitHub"
12+
task :update do
13+
require "net/http"
14+
require "uri"
15+
uri = URI("https://raw.githubusercontent.com/PaystackOSS/openapi/main/dist/paystack.yaml")
16+
content = Net::HTTP.get(uri)
17+
File.write("openapi/paystack.yaml", content)
18+
puts "Updated openapi/paystack.yaml"
19+
end
20+
end
21+
22+
desc "Generate resource files from openapi/paystack.yaml"
23+
task :generate do
24+
ruby "generator/generate.rb"
25+
end
26+
1027
task default: %i[test standard]

generator/generate.rb

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "yaml"
5+
require "erb"
6+
require "fileutils"
7+
8+
SPEC_PATH = File.expand_path("../openapi/paystack.yaml", __dir__)
9+
TEMPLATE_PATH = File.expand_path("templates/resource.erb", __dir__)
10+
OUTPUT_DIR = File.expand_path("../lib/paystack/resources", __dir__)
11+
12+
RESOURCES = %w[Transaction Customer].freeze
13+
14+
def snake_case(str)
15+
str
16+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
17+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
18+
.downcase
19+
end
20+
21+
# Strip the tag prefix from an operationId and convert to snake_case.
22+
# e.g. "transaction_chargeAuthorization" -> "charge_authorization"
23+
# "customer_riskAction" -> "risk_action"
24+
def method_name(operation_id, tag)
25+
without_prefix = operation_id.sub(/\A#{Regexp.escape(tag)}_/i, "")
26+
snake_case(without_prefix)
27+
end
28+
29+
# Build the method signature from path params and whether the op has a body or query.
30+
def build_signature(path_params, http_method, has_body)
31+
parts = path_params.map { |p| "#{snake_case(p)}:" }
32+
33+
if has_body
34+
parts << "**params"
35+
elsif %w[get delete].include?(http_method)
36+
parts << "**params"
37+
end
38+
39+
parts.join(", ")
40+
end
41+
42+
# Build the transport path, interpolating path params as Ruby string interpolation.
43+
def build_transport_path(path, path_params)
44+
result = path.dup
45+
path_params.each do |p|
46+
result = result.gsub("{#{p}}", "\#{#{snake_case(p)}}")
47+
end
48+
result
49+
end
50+
51+
# Build the trailing args for the transport call.
52+
def build_transport_args(http_method, has_body, path_params)
53+
uses_params = has_body || %w[get delete].include?(http_method)
54+
return "" unless uses_params
55+
56+
if has_body
57+
", body: params"
58+
else
59+
", query: params"
60+
end
61+
end
62+
63+
spec = YAML.load_file(SPEC_PATH, aliases: true)
64+
template = ERB.new(File.read(TEMPLATE_PATH), trim_mode: "-")
65+
66+
RESOURCES.each do |tag|
67+
operations = []
68+
69+
spec["paths"].each do |path, path_item|
70+
# Path-level parameters (shared across methods on this path)
71+
shared_params = (path_item["parameters"] || [])
72+
.select { |p| p.is_a?(Hash) && p["in"] == "path" }
73+
.map { |p| p["name"] }
74+
75+
path_item.each do |http_method, op|
76+
next unless op.is_a?(Hash) && op["tags"]&.include?(tag)
77+
78+
op_path_params = shared_params + op.fetch("parameters", [])
79+
.select { |p| p.is_a?(Hash) && p["in"] == "path" }
80+
.map { |p| p["name"] }
81+
82+
has_body = op.key?("requestBody")
83+
op_id = op["operationId"]
84+
name = method_name(op_id, tag)
85+
86+
transport_path = build_transport_path(path, op_path_params)
87+
uses_params = has_body || %w[get delete].include?(http_method)
88+
89+
operations << {
90+
summary: op["summary"] || op_id,
91+
http_method: http_method,
92+
path: path,
93+
method_name: name,
94+
signature: build_signature(op_path_params, http_method, has_body),
95+
transport_path: transport_path,
96+
transport_args: build_transport_args(http_method, has_body, op_path_params),
97+
uses_params: uses_params
98+
}
99+
end
100+
end
101+
102+
class_name = tag
103+
output = template.result(binding)
104+
105+
FileUtils.mkdir_p(OUTPUT_DIR)
106+
out_path = File.join(OUTPUT_DIR, "#{snake_case(tag)}.rb")
107+
File.write(out_path, output)
108+
puts "Generated #{out_path} (#{operations.size} operations)"
109+
end

generator/templates/resource.erb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
# Generated by generator/generate.rb — do not edit manually
3+
4+
module Paystack
5+
module Resources
6+
class <%= class_name %> < BaseResource
7+
<% operations.each do |op| %>
8+
# <%= op[:summary] %>
9+
# <%= op[:http_method].upcase %> <%= op[:path] %>
10+
def <%= op[:method_name] %>(<%= op[:signature] %>)
11+
@transport.<%= op[:http_method] %>("<%= op[:transport_path] %>"<%= op[:transport_args] %>)
12+
end
13+
<% if op[:method_name] == "initialize" %>
14+
public :initialize
15+
<% end %>
16+
<% end %>
17+
end
18+
end
19+
end

lib/paystack.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "paystack/version"
4+
require_relative "paystack/errors"
5+
require_relative "paystack/response"
6+
require_relative "paystack/transport"
7+
require_relative "paystack/base_resource"
8+
require_relative "paystack/resources/transaction"
9+
require_relative "paystack/resources/customer"
10+
require_relative "paystack/client"
11+
12+
module Paystack
13+
end

lib/paystack/base_resource.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
module Paystack
4+
class BaseResource
5+
# Override .new so subclasses can use #initialize as an API method
6+
# without conflicting with Ruby's constructor protocol.
7+
def self.new(transport)
8+
instance = allocate
9+
instance.instance_variable_set(:@transport, transport)
10+
instance
11+
end
12+
end
13+
end

lib/paystack/client.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
module Paystack
4+
class Client
5+
def initialize(api_key)
6+
@transport = Transport.new(api_key)
7+
end
8+
9+
def transaction
10+
@transaction ||= Resources::Transaction.new(@transport)
11+
end
12+
13+
def customer
14+
@customer ||= Resources::Customer.new(@transport)
15+
end
16+
end
17+
end

lib/paystack/errors.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module Paystack
4+
class Error < StandardError
5+
attr_reader :http_status, :data
6+
7+
def initialize(message = nil, http_status: nil, data: nil)
8+
super(message)
9+
@http_status = http_status
10+
@data = data
11+
end
12+
end
13+
14+
AuthenticationError = Class.new(Error)
15+
ValidationError = Class.new(Error)
16+
NotFoundError = Class.new(Error)
17+
RateLimitError = Class.new(Error)
18+
ServerError = Class.new(Error)
19+
end

0 commit comments

Comments
 (0)