Skip to content

Commit 34f00d6

Browse files
committed
ORM Style Interface
1 parent 7c15a42 commit 34f00d6

27 files changed

Lines changed: 574 additions & 0 deletions

lib/recurly.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
require "recurly/connection_pool"
1111
require "recurly/client"
1212
require "recurly/webhooks"
13+
require "recurly/config"
14+
require "recurly/models"
1315

1416
module Recurly
1517
STRICT_MODE = ENV["RECURLY_STRICT_MODE"] && ENV["RECURLY_STRICT_MODE"].downcase == "true"

lib/recurly/config.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
module Recurly
4+
# Configuration class for Recurly API client.
5+
class Config
6+
@@client = nil
7+
8+
def self.client=(val)
9+
@@client = val
10+
end
11+
12+
def self.api_key
13+
ENV["RECURLY_API_KEY"]
14+
end
15+
16+
def self.host
17+
ENV["RECURLY_HOST"] || "v3.recurly.com"
18+
end
19+
20+
def self.port
21+
ENV["RECURLY_PORT"] || "443"
22+
end
23+
24+
def self.region
25+
ENV["RECURLY_REGION"]&.to_sym || :us
26+
end
27+
28+
def self.debug?
29+
ENV["RECURLY_DEBUG"].to_s.upcase == "TRUE"
30+
end
31+
32+
def self.base_url
33+
"https://#{host}:#{port}"
34+
end
35+
36+
def self.ca_file
37+
return unless host.end_with?(".recurly.dev")
38+
39+
@ca_file ||= begin
40+
path = File.join(File.dirname(__FILE__), "../../../../", "certs/ca_root.crt")
41+
File.exist?(path) ? path : (raise "CA file does not exist: #{path}")
42+
end
43+
end
44+
45+
def self.client
46+
return @@client if @@client
47+
48+
opt = {
49+
api_key: api_key,
50+
logger: Logger.new(STDOUT).tap { |l| l.level = debug? ? Logger::DEBUG : Logger::INFO },
51+
region: region,
52+
base_url: base_url,
53+
}
54+
opt[:ca_file] = ca_file if ca_file
55+
@client ||= Recurly::Client.new(**opt)
56+
end
57+
end
58+
end

lib/recurly/model.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# frozen_string_literal: true
2+
3+
module Recurly
4+
# Base model class for Recurly ORM Interface.
5+
class Model
6+
API_KEYS = %i[limit sort order begin_time end_time].freeze
7+
attr_accessor :resource
8+
9+
def initialize(res)
10+
@resource = res
11+
end
12+
13+
def self.client
14+
cli = Config.client
15+
raise "API client is not configured" unless cli
16+
17+
cli
18+
end
19+
20+
# Lookup methods which should be implemented in subclasses.
21+
def self.list(params)
22+
raise NotImplementedError, "The list method should be implemented in the subclass."
23+
end
24+
25+
def self.get(id:)
26+
raise NotImplementedError, "The get method should be implemented in the subclass."
27+
end
28+
29+
# Lookup methods
30+
def self.all
31+
where
32+
end
33+
34+
def self.where(query = query_params, *args)
35+
if query.is_a?(String)
36+
conditions = QueryParser.parse(query, *args)
37+
api_params = {}
38+
filter_params = []
39+
conditions.each do |cond|
40+
val = cond[:value].respond_to?(:iso8601) ? cond[:value].iso8601 : cond[:value]
41+
if API_KEYS.include?(cond[:key].to_sym)
42+
api_params[cond[:key].to_sym] = val
43+
else
44+
filter_params << cond.merge(value: val)
45+
end
46+
end
47+
pager = list(params: api_params)
48+
ModelPager.new(model_class: self, pager: pager, filters: filter_params)
49+
else
50+
api_params = (query || {}).select { |k, _| API_KEYS.include?(k) }
51+
pager = list(params: api_params)
52+
ModelPager.new(model_class: self, pager: pager)
53+
end
54+
end
55+
56+
def self.find_by(args = {})
57+
args[:id] ? new(get(id: args[:id])) : nil
58+
rescue Recurly::Errors::NotFoundError
59+
nil
60+
end
61+
62+
# Default query parameters for listing or searching models.
63+
def self.query_params(args = nil)
64+
params = { limit: 200 }
65+
if args
66+
params[:limit] = args[:limit] if args[:limit]
67+
params[:sort] = args[:sort] if args[:sort]
68+
params[:order] = args[:order] if args[:order]
69+
params[:begin_time] = args[:begin_time].iso8601 if args[:begin_time]
70+
params[:end_time] = args[:end_time].iso8601 if args[:end_time]
71+
end
72+
params
73+
end
74+
75+
# Method delegation to the resource for all methods not defined in this or child classes.
76+
def method_missing(method, *args, &block)
77+
if @resource.respond_to?(method)
78+
@resource.public_send(method, *args, &block)
79+
else
80+
super
81+
end
82+
end
83+
84+
def respond_to_missing?(method, include_private = false)
85+
@resource.respond_to?(method, include_private) || super
86+
end
87+
end
88+
end

lib/recurly/model_filter.rb

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# frozen_string_literal: true
2+
3+
module Recurly
4+
# Handles filtering logic for ModelPager
5+
class ModelFilter
6+
def initialize(filters)
7+
@filters = filters
8+
end
9+
10+
def include?(model)
11+
return true if @filters.nil? || @filters.empty?
12+
13+
@filters.all? do |cond|
14+
val = cond[:key].match(/\./) ? dig_value(model, cond[:key]) : model.public_send(cond[:key])
15+
cmp_val = cond[:value]
16+
17+
val_parsed = try_parse(val)
18+
cmp_parsed = try_parse(cmp_val)
19+
20+
case cond[:op]
21+
when "="
22+
val_parsed == cmp_parsed
23+
when "!="
24+
val_parsed != cmp_parsed
25+
when ">", ">=", "<", "<="
26+
return false if val_parsed.nil? || cmp_parsed.nil?
27+
28+
case cond[:op]
29+
when ">"
30+
val_parsed > cmp_parsed
31+
when ">="
32+
val_parsed >= cmp_parsed
33+
when "<"
34+
val_parsed < cmp_parsed
35+
when "<="
36+
val_parsed <= cmp_parsed
37+
end
38+
else
39+
false
40+
end
41+
end
42+
end
43+
44+
private
45+
46+
# Recursively dig through nested attributes and arrays
47+
def dig_value(obj, key_chain)
48+
keys = key_chain.to_s.split(".")
49+
current = obj
50+
keys.each do |key|
51+
if current.is_a?(Array)
52+
current = current.map { |el| dig_value(el, key) }.flatten.compact
53+
elsif current.respond_to?(key)
54+
current = current.public_send(key)
55+
else
56+
return nil
57+
end
58+
end
59+
current.is_a?(Array) && current.size == 1 ? current.first : current
60+
end
61+
62+
def try_parse(value)
63+
begin
64+
return DateTime.parse(value) if value.is_a?(String)
65+
rescue ArgumentError; end
66+
begin
67+
return Integer(value) if value.to_s.match(/\A-?\d+\z/)
68+
rescue ArgumentError, TypeError; end
69+
begin
70+
return Float(value) if value.to_s.match(/\A-?\d+(\.\d+)?\z/)
71+
rescue ArgumentError, TypeError; end
72+
value
73+
end
74+
end
75+
end

lib/recurly/model_pager.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
module Recurly
4+
# Pager extension for Recurly ORM Interface models, allowing iteration over model instances.
5+
class ModelPager < Pager
6+
include Enumerable
7+
8+
def initialize(model_class:, pager:, filters: [])
9+
@model_class = model_class
10+
@filter = ModelFilter.new(filters)
11+
super(client: pager.client, path: pager.next, options: pager.instance_variable_get(:@options))
12+
end
13+
14+
def each(&block)
15+
super do |item|
16+
model = @model_class.new(item)
17+
block.call(model) if @filter.include?(model)
18+
end
19+
end
20+
21+
def each_page(&block)
22+
super do |page|
23+
transformed_page = page.each_with_object([]) do |item, result|
24+
model = @model_class.new(item)
25+
result << model if @filter.include?(model)
26+
end
27+
block.call(transformed_page) if block_given?
28+
end
29+
end
30+
end
31+
end

lib/recurly/models.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require_relative "query_parser"
2+
require_relative "model_pager"
3+
require_relative "model_filter"
4+
require_relative "model"
5+
Dir[File.join(__dir__, "models", "*.rb")].each do |file|
6+
require file unless file.end_with?("model.rb")
7+
end

lib/recurly/models/account.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
module Recurly
4+
# Represents an Account in Recurly
5+
class Account < Recurly::Model
6+
def self.list(params:)
7+
client.list_accounts(params: params)
8+
end
9+
10+
def self.get(id:)
11+
client.get_account(account_id: id)
12+
end
13+
end
14+
end

lib/recurly/models/add_on.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
module Recurly
4+
# Represents a AddOn in Recurly
5+
class AddOn < Recurly::Model
6+
def self.list(params:)
7+
client.list_add_ons(params: params)
8+
end
9+
10+
def self.get(id:)
11+
client.get_add_on(add_on_id: id)
12+
end
13+
end
14+
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
module Recurly
4+
# Represents a BusinessEntity in Recurly
5+
class BusinessEntity < Recurly::Model
6+
def self.list(params:)
7+
client.list_business_entities(params: params)
8+
end
9+
10+
def self.get(id:)
11+
client.get_business_entity(business_entity_id: id)
12+
end
13+
end
14+
end

lib/recurly/models/coupon.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
module Recurly
4+
# Represents a Coupon in Recurly
5+
class Coupon < Recurly::Model
6+
def self.list(params:)
7+
client.list_coupons(params: params)
8+
end
9+
10+
def self.get(id:)
11+
client.get_coupon(coupon_id: id)
12+
end
13+
end
14+
end

0 commit comments

Comments
 (0)