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
1 change: 1 addition & 0 deletions lib/stripe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
require "stripe/event_types"
require "stripe/request_options"
require "stripe/request_params"
require "stripe/stripe_context"
require "stripe/util"
require "stripe/connection_manager"
require "stripe/multipart_encoder"
Expand Down
4 changes: 3 additions & 1 deletion lib/stripe/event_notification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ def initialize(event_payload, client)
@type = event_payload[:type]
@created = event_payload[:created]
@livemode = event_payload[:livemode]
@context = event_payload[:context]
@reason = EventReason.new(event_payload[:reason]) if event_payload[:reason]
if event_payload[:context] && !event_payload[:context].empty?
@context = StripeContext.parse(event_payload[:context])
end
# private unless a child declares an attr_reader
@related_object = RelatedObject.new(event_payload[:related_object]) if event_payload[:related_object]

Expand Down
15 changes: 13 additions & 2 deletions lib/stripe/request_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ module RequestOptions
OPTS_USER_SPECIFIED - Set[:idempotency_key, :stripe_context]
).freeze

# helper method to figure out what the true value of the stripe_context header should be
# given a pair of StripeContext|string
# req should take precedence if non-nil
private_class_method def self.merge_context(config_ctx, req_ctx)
str_with_precedence = (req_ctx || config_ctx)&.to_s
return nil if str_with_precedence.nil? || str_with_precedence.empty?

str_with_precedence
end

# Merges requestor options on a StripeConfiguration object
# with a per-request options hash, giving precedence
# to the per-request options. Expects StripeConfiguration and hash.
Expand All @@ -42,7 +52,7 @@ def self.merge_config_and_opts(config, req_opts)
api_key: req_opts[:api_key] || config.api_key,
idempotency_key: req_opts[:idempotency_key],
stripe_account: req_opts[:stripe_account] || config.stripe_account,
stripe_context: req_opts[:stripe_context] || config.stripe_context,
stripe_context: merge_context(config.stripe_context, req_opts[:stripe_context]),
stripe_version: req_opts[:stripe_version] || config.api_version,
headers: req_opts[:headers] || {},
}
Expand All @@ -62,7 +72,7 @@ def self.combine_opts(object_opts, req_opts)
api_key: req_opts[:api_key] || object_opts[:api_key],
idempotency_key: req_opts[:idempotency_key],
stripe_account: req_opts[:stripe_account] || object_opts[:stripe_account],
stripe_context: req_opts[:stripe_context] || object_opts[:stripe_context],
stripe_context: merge_context(object_opts[:stripe_context], req_opts[:stripe_context]),
stripe_version: req_opts[:stripe_version] || object_opts[:stripe_version],
headers: req_opts[:headers] || {},
}
Expand Down Expand Up @@ -100,6 +110,7 @@ def self.error_on_non_string_user_opts(normalized_opts)
val = normalized_opts[opt]
next if val.nil?
next if val.is_a?(String)
next if opt == :stripe_context && val.is_a?(StripeContext)

raise ArgumentError,
"request option '#{opt}' should be a string value " \
Expand Down
68 changes: 68 additions & 0 deletions lib/stripe/stripe_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true

module Stripe
# Represents hierarchical context for Stripe API operations.
#
# This class is immutable - all methods return new instances rather than
# modifying the existing instance. It provides utilities for building
# context hierarchies and converting to/from string representations.
class StripeContext
include Comparable

attr_reader :segments

# Creates a new StripeContext with the given segments.
def initialize(segments = nil)
@segments = (segments || []).map(&:to_s).freeze
end

# Parses a context string into a StripeContext instance.
def self.parse(context_str)
return new if context_str.nil? || context_str.empty?

new(context_str.split("/"))
end

# Creates a new StripeContext with an additional segment appended.
def push(segment)
segment_str = segment.to_s.strip
raise ArgumentError, "Segment cannot be empty or whitespace" if segment_str.empty?

new_segments = @segments + [segment_str]
self.class.new(new_segments)
end

# Creates a new StripeContext with the last segment removed.
# If there are no segments, returns a new empty StripeContext.
def pop
raise IndexError, "No segments to pop" if @segments.empty?

new_segments = @segments[0...-1]
self.class.new(new_segments)
end

# Converts this context to its string representation.
def to_s
@segments.join("/")
end

# Checks equality with another StripeContext.
def ==(other)
other.is_a?(StripeContext) && @segments == other.segments
end

# Alias for == to support eql? method
alias eql? ==

# Returns a human-readable representation for debugging.
def inspect
"#<#{self.class}:0x#{object_id.to_s(16)} segments=#{@segments.inspect}>"
end

# Returns true if the context has no segments.
# @return [Boolean] true if empty, false otherwise
def empty?
@segments.empty?
end
end
end
Loading