Merge pull request #586 from stripe/brandur-remove-marshal

Implement deep copy for StripeObject and remove marshal/unmarshal
This commit is contained in:
Brandur 2017-09-29 07:13:32 -07:00 committed by GitHub
commit 3f454495bf
5 changed files with 116 additions and 42 deletions

View File

@ -35,10 +35,10 @@ Metrics/LineLength:
Metrics/MethodLength: Metrics/MethodLength:
Max: 47 Max: 47
# Offense count: 1 # Offense count: 2
# Configuration parameters: CountComments. # Configuration parameters: CountComments.
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 290 Max: 423
# Offense count: 6 # Offense count: 6
# Configuration parameters: CountKeywordArgs. # Configuration parameters: CountKeywordArgs.
@ -51,11 +51,6 @@ Metrics/PerceivedComplexity:
Exclude: Exclude:
- 'lib/stripe/stripe_object.rb' - 'lib/stripe/stripe_object.rb'
# Offense count: 1
Security/MarshalLoad:
Exclude:
- 'lib/stripe/stripe_object.rb'
# Offense count: 2 # Offense count: 2
Style/ClassVars: Style/ClassVars:
Exclude: Exclude:

View File

@ -2,8 +2,6 @@ module Stripe
module APIOperations module APIOperations
module Request module Request
module ClassMethods module ClassMethods
OPTS_KEYS_TO_PERSIST = Set[:api_key, :api_base, :client, :stripe_account, :stripe_version]
def request(method, url, params = {}, opts = {}) def request(method, url, params = {}, opts = {})
warn_on_opts_in_params(params) warn_on_opts_in_params(params)
@ -25,7 +23,7 @@ module Stripe
# Hash#select returns an array before 1.9 # Hash#select returns an array before 1.9
opts_to_persist = {} opts_to_persist = {}
opts.each do |k, v| opts.each do |k, v|
opts_to_persist[k] = v if OPTS_KEYS_TO_PERSIST.include?(k) opts_to_persist[k] = v if Util::OPTS_KEYS_TO_PERSIST.include?(k)
end end
[resp, opts_to_persist] [resp, opts_to_persist]
@ -33,11 +31,8 @@ module Stripe
private private
KNOWN_OPTS = Set[:api_key, :idempotency_key, :stripe_account, :stripe_version]
private_constant :KNOWN_OPTS
def warn_on_opts_in_params(params) def warn_on_opts_in_params(params)
KNOWN_OPTS.each do |opt| Util::OPTS_USER_SPECIFIED.each do |opt|
if params.key?(opt) if params.key?(opt)
$stderr.puts("WARNING: #{opt} should be in opts instead of params.") $stderr.puts("WARNING: #{opt} should be in opts instead of params.")
end end

View File

@ -133,20 +133,6 @@ module Stripe
@values.each(&blk) @values.each(&blk)
end end
def _dump(_level)
# The StripeClient instance in @opts is not serializable and is not
# really a property of the StripeObject, so we exclude it when
# dumping
opts = @opts.clone
opts.delete(:client)
Marshal.dump([@values, opts])
end
def self._load(args)
values, opts = Marshal.load(args)
construct_from(values, opts)
end
# Sets all keys within the StripeObject as unsaved so that they will be # Sets all keys within the StripeObject as unsaved so that they will be
# included with an update when #serialize_params is called. This method is # included with an update when #serialize_params is called. This method is
# also recursive, so any StripeObjects contained as values or which are # also recursive, so any StripeObjects contained as values or which are
@ -305,7 +291,9 @@ module Stripe
# remove accessors. # remove accessors.
def initialize_from(values, opts, partial = false) def initialize_from(values, opts, partial = false)
@opts = Util.normalize_opts(opts) @opts = Util.normalize_opts(opts)
@original_values = Marshal.load(Marshal.dump(values)) # deep copy
# the `#send` is here so that we can keep this method private
@original_values = self.class.send(:deep_copy, values)
removed = partial ? Set.new : Set.new(@values.keys - values.keys) removed = partial ? Set.new : Set.new(@values.keys - values.keys)
added = Set.new(values.keys - @values.keys) added = Set.new(values.keys - @values.keys)
@ -405,6 +393,30 @@ module Stripe
private private
# Produces a deep copy of the given object including support for arrays,
# hashes, and StripeObjects.
def self.deep_copy(obj)
case obj
when Array
obj.map { |e| deep_copy(e) }
when Hash
obj.each_with_object({}) do |(k, v), copy|
copy[k] = deep_copy(v)
copy
end
when StripeObject
StripeObject.construct_from(
deep_copy(obj.instance_variable_get(:@values)),
obj.instance_variable_get(:@opts).select do |k, _v|
Util::OPTS_COPYABLE.include?(k)
end
)
else
obj
end
end
private_class_method :deep_copy
def dirty_value!(value) def dirty_value!(value)
case value case value
when Array when Array

View File

@ -2,6 +2,22 @@ require "cgi"
module Stripe module Stripe
module Util module Util
# Options that a user is allowed to specify.
OPTS_USER_SPECIFIED = Set[
:api_key,
:idempotency_key,
:stripe_account,
:stripe_version
].freeze
# Options that should be copyable from one StripeObject to another
# including options that may be internal.
OPTS_COPYABLE = (OPTS_USER_SPECIFIED + Set[:api_base]).freeze
# Options that should be persisted between API requests. This includes
# client, which is an object containing an HTTP client to reuse.
OPTS_KEYS_TO_PERSIST = (OPTS_USER_SPECIFIED + Set[:client]).freeze
def self.objects_to_ids(h) def self.objects_to_ids(h)
case h case h
when APIResource when APIResource

View File

@ -35,19 +35,75 @@ module Stripe
assert_equal "Stripe", obj[:name] assert_equal "Stripe", obj[:name]
end end
should "marshal a stripe object correctly" do context "#deep_copy" do
obj = Stripe::StripeObject.construct_from( should "produce a deep copy" do
{ id: 1, name: "Stripe" }, opts = {
api_key: "apikey", api_base: Stripe.api_base,
# StripeClient is not serializable and is expected to be removed api_key: "apikey",
# when marshalling StripeObjects }
client: StripeClient.active_client values = {
) id: 1,
m = Marshal.load(Marshal.dump(obj)) name: "Stripe",
assert_equal 1, m.id arr: [
assert_equal "Stripe", m.name StripeObject.construct_from({ id: "index0" }, opts),
expected_hash = { api_key: "apikey" } "index1",
assert_equal expected_hash, m.instance_variable_get("@opts") 2,
],
map: {
:"0" => StripeObject.construct_from({ id: "index0" }, opts),
:"1" => "index1",
:"2" => 2,
},
}
# it's not good to test methods with `#send` like this, but I've done
# it in the interest of trying to keep `.deep_copy` as internal as
# possible
copy_values = Stripe::StripeObject.send(:deep_copy, values)
# we can't compare the hashes directly because they have embedded
# objects which are different from each other
assert_equal values[:id], copy_values[:id]
assert_equal values[:name], copy_values[:name]
assert_equal values[:arr].length, copy_values[:arr].length
# internal values of the copied StripeObject should be the same
# (including opts), but the object itself should be new (hence the
# refutation of equality on #object_id)
assert_equal values[:arr][0][:id], copy_values[:arr][0][:id]
refute_equal values[:arr][0].object_id, copy_values[:arr][0].object_id
assert_equal values[:arr][0].instance_variable_get(:@opts),
copy_values[:arr][0].instance_variable_get(:@opts)
# scalars however, can be compared
assert_equal values[:arr][1], copy_values[:arr][1]
assert_equal values[:arr][2], copy_values[:arr][2]
# and a similar story with the hash
assert_equal values[:map].keys, copy_values[:map].keys
assert_equal values[:map][:"0"][:id], copy_values[:map][:"0"][:id]
refute_equal values[:map][:"0"].object_id, copy_values[:map][:"0"].object_id
assert_equal values[:map][:"0"].instance_variable_get(:@opts),
copy_values[:map][:"0"].instance_variable_get(:@opts)
assert_equal values[:map][:"1"], copy_values[:map][:"1"]
assert_equal values[:map][:"2"], copy_values[:map][:"2"]
end
should "not copy a client" do
opts = {
api_key: "apikey",
client: StripeClient.active_client,
}
values = { id: 1, name: "Stripe" }
obj = Stripe::StripeObject.construct_from(values, opts)
copy_obj = Stripe::StripeObject.send(:deep_copy, obj)
assert_equal values, copy_obj.instance_variable_get(:@values)
assert_equal opts.reject { |k, _v| k == :client },
copy_obj.instance_variable_get(:@opts)
end
end end
should "recursively call to_hash on its values" do should "recursively call to_hash on its values" do