Skip to content
Merged
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
2 changes: 0 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ PATH
paddle (2.10)
cgi
faraday (>= 2.14.3, < 3)
ostruct (~> 0.6.0)

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -45,7 +44,6 @@ GEM
minitest (5.27.0)
net-http (0.9.1)
uri (>= 0.11.1)
ostruct (0.6.3)
parallel (2.1.0)
parser (3.3.11.1)
ast (~> 2.4.1)
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,21 @@ end

The gem maps as closely as we can to the Paddle API so you can easily convert API examples to gem code.

Responses are created as objects like `Paddle::Product`. Having types like `Paddle::Product` is handy for understanding what type of object you're working with. They're built using OpenStruct so you can easily access data in a Ruby-ish way.
Responses are created as objects like `Paddle::Product`. Having types like `Paddle::Product` is handy for understanding what type of object you're working with. Attributes can be read with dot notation or like a hash, and nested data is wrapped too:

```ruby
transaction = Paddle::Transaction.retrieve(id: "txn_abc123")

transaction.status #=> "completed"
transaction[:status] #=> "completed"
transaction.details.totals.total #=> "1000"
transaction.dig(:items, 0, :price, :id) #=> "pri_abc123"
transaction.missing_attribute #=> nil

# Convert back to a hash or JSON, including nested data
transaction.to_h
transaction.to_json
```

### Pagination

Expand Down
107 changes: 91 additions & 16 deletions lib/paddle/object.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,66 @@
require "ostruct"

module Paddle
class Object < OpenStruct
def initialize(attributes)
super to_ostruct(attributes)
end

def to_ostruct(obj)
if obj.is_a?(Hash)
OpenStruct.new(obj.map { |key, val| [ key, to_ostruct(val) ] }.to_h)
elsif obj.is_a?(Array)
obj.map { |o| to_ostruct(o) }
else # Assumed to be a primitive value
obj
end
# A lightweight wrapper around API response data. Attributes can be read with dot notation
# (object.id) or like a hash (object[:id] or object["id"]). Nested hashes are wrapped too.
class Object
def initialize(attributes = {})
@attributes = {}
(attributes || {}).each { |key, val| self[key] = val }
end

def [](key)
@attributes[key.to_sym]
end

def []=(key, val)
@attributes[key.to_sym] = wrap(val)
end

def key?(key)
@attributes.key?(key.to_sym)
end

def dig(key, *rest)
val = self[key]
rest.empty? || val.nil? ? val : val.dig(*rest)
end

def each_pair(&block)
return enum_for(:each_pair) unless block_given?

@attributes.each_pair(&block)
self
end

# Returns the attributes as a hash, with nested objects converted to hashes too
def to_h
@attributes.transform_values { |val| unwrap(val) }
end

def as_json(*)
to_h
end

def to_json(*args)
to_h.to_json(*args)
end

def ==(other)
other.is_a?(Paddle::Object) && to_h == other.to_h
end
alias eql? ==

def hash
to_h.hash
end

def inspect
attrs = @attributes.map { |key, val| "#{key}=#{val.inspect}" }.join(", ")
"#<#{self.class.name}#{" " unless attrs.empty?}#{attrs}>"
end
alias to_s inspect

def update(**params)
method_missing :update unless klass.respond_to? :update
raise NoMethodError, "undefined method 'update' for #{klass}" unless klass.respond_to? :update

primary_attributes = klass.method(:update).parameters.select { |(type, name)| type == :keyreq }.map(&:last) # Identified by whatever is a required named parameter.

Expand All @@ -36,5 +79,37 @@ def update(**params)
def klass
self.class
end

# Unknown attributes return nil, like a hash
def method_missing(name, *args)
if name.end_with?("=") && args.size == 1
self[name.to_s.chomp("=")] = args.first
elsif args.empty? && !name.end_with?("=", "?", "!")
self[name]
else
super
end
end

def respond_to_missing?(name, include_private = false)
key?(name.to_s.chomp("=")) || super
end

def wrap(val)
case val
when Paddle::Object then val
when Hash then Paddle::Object.new(val)
when Array then val.map { |v| wrap(v) }
else val
end
end

def unwrap(val)
case val
when Paddle::Object then val.to_h
when Array then val.map { |v| unwrap(v) }
else val
end
end
end
end
1 change: 0 additions & 1 deletion paddle.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,5 @@ Gem::Specification.new do |spec|
spec.require_paths = [ "lib" ]

spec.add_dependency "faraday", ">= 2.14.3", "< 3"
spec.add_dependency "ostruct", "~> 0.6.0"
spec.add_dependency "cgi"
end
58 changes: 57 additions & 1 deletion test/paddle/object_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,63 @@ def test_nested_number

def test_array
object = Paddle::Object.new(foo: [ { bar: :baz } ])
assert_equal OpenStruct, object.foo.first.class
assert_equal Paddle::Object, object.foo.first.class
assert_equal :baz, object.foo.first.bar
end

def test_hash_access
object = Paddle::Object.new("id" => "txn_abc123")
assert_equal "txn_abc123", object[:id]
assert_equal "txn_abc123", object["id"]
end

def test_missing_attribute_returns_nil
object = Paddle::Object.new(foo: "bar")
assert_nil object.baz
refute object.respond_to?(:baz)
assert object.respond_to?(:foo)
end

def test_setting_attributes
object = Paddle::Object.new(foo: "bar")
object.foo = "baz"
object[:qux] = { quux: 1 }
assert_equal "baz", object.foo
assert_equal 1, object.qux.quux
end

def test_attributes_named_after_private_methods
object = Paddle::Object.new(format: "pdf", test: true)
assert_equal "pdf", object.format
assert_equal true, object.test
end

def test_unknown_method_with_arguments_raises
assert_raises(NoMethodError) { Paddle::Object.new(foo: "bar").foo(1) }
end

def test_dig
object = Paddle::Object.new(items: [ { price: { id: "pri_abc123" } } ])
assert_equal "pri_abc123", object.dig(:items, 0, :price, :id)
assert_nil object.dig(:missing, :id)
end

def test_to_h_converts_nested_objects
object = Paddle::Object.new(foo: { bar: [ { baz: 1 } ] })
assert_equal({ foo: { bar: [ { baz: 1 } ] } }, object.to_h)
end

def test_to_json
object = Paddle::Object.new(foo: { bar: "baz" })
assert_equal '{"foo":{"bar":"baz"}}', object.to_json
end

def test_equality
assert_equal Paddle::Object.new(foo: { bar: 1 }), Paddle::Object.new("foo" => { "bar" => 1 })
refute_equal Paddle::Object.new(foo: 1), Paddle::Object.new(foo: 2)
end

def test_nil_attributes
assert_equal({}, Paddle::Object.new(nil).to_h)
end
end
Loading