Skip to content

Commit 71858a8

Browse files
Add StripeContext object (stripe#1664)
* add stripe context * pr feedback
1 parent 0f9faf2 commit 71858a8

5 files changed

Lines changed: 585 additions & 3 deletions

File tree

lib/stripe.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
require "stripe/event_types"
3535
require "stripe/request_options"
3636
require "stripe/request_params"
37+
require "stripe/stripe_context"
3738
require "stripe/util"
3839
require "stripe/connection_manager"
3940
require "stripe/multipart_encoder"

lib/stripe/event_notification.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ def initialize(event_payload, client)
3838
@type = event_payload[:type]
3939
@created = event_payload[:created]
4040
@livemode = event_payload[:livemode]
41-
@context = event_payload[:context]
4241
@reason = EventReason.new(event_payload[:reason]) if event_payload[:reason]
42+
if event_payload[:context] && !event_payload[:context].empty?
43+
@context = StripeContext.parse(event_payload[:context])
44+
end
4345
# private unless a child declares an attr_reader
4446
@related_object = RelatedObject.new(event_payload[:related_object]) if event_payload[:related_object]
4547

lib/stripe/request_options.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ module RequestOptions
2929
OPTS_USER_SPECIFIED - Set[:idempotency_key, :stripe_context]
3030
).freeze
3131

32+
# helper method to figure out what the true value of the stripe_context header should be
33+
# given a pair of StripeContext|string
34+
# req should take precedence if non-nil
35+
private_class_method def self.merge_context(config_ctx, req_ctx)
36+
str_with_precedence = (req_ctx || config_ctx)&.to_s
37+
return nil if str_with_precedence.nil? || str_with_precedence.empty?
38+
39+
str_with_precedence
40+
end
41+
3242
# Merges requestor options on a StripeConfiguration object
3343
# with a per-request options hash, giving precedence
3444
# to the per-request options. Expects StripeConfiguration and hash.
@@ -42,7 +52,7 @@ def self.merge_config_and_opts(config, req_opts)
4252
api_key: req_opts[:api_key] || config.api_key,
4353
idempotency_key: req_opts[:idempotency_key],
4454
stripe_account: req_opts[:stripe_account] || config.stripe_account,
45-
stripe_context: req_opts[:stripe_context] || config.stripe_context,
55+
stripe_context: merge_context(config.stripe_context, req_opts[:stripe_context]),
4656
stripe_version: req_opts[:stripe_version] || config.api_version,
4757
headers: req_opts[:headers] || {},
4858
}
@@ -62,7 +72,7 @@ def self.combine_opts(object_opts, req_opts)
6272
api_key: req_opts[:api_key] || object_opts[:api_key],
6373
idempotency_key: req_opts[:idempotency_key],
6474
stripe_account: req_opts[:stripe_account] || object_opts[:stripe_account],
65-
stripe_context: req_opts[:stripe_context] || object_opts[:stripe_context],
75+
stripe_context: merge_context(object_opts[:stripe_context], req_opts[:stripe_context]),
6676
stripe_version: req_opts[:stripe_version] || object_opts[:stripe_version],
6777
headers: req_opts[:headers] || {},
6878
}
@@ -100,6 +110,7 @@ def self.error_on_non_string_user_opts(normalized_opts)
100110
val = normalized_opts[opt]
101111
next if val.nil?
102112
next if val.is_a?(String)
113+
next if opt == :stripe_context && val.is_a?(StripeContext)
103114

104115
raise ArgumentError,
105116
"request option '#{opt}' should be a string value " \

lib/stripe/stripe_context.rb

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# frozen_string_literal: true
2+
3+
module Stripe
4+
# Represents hierarchical context for Stripe API operations.
5+
#
6+
# This class is immutable - all methods return new instances rather than
7+
# modifying the existing instance. It provides utilities for building
8+
# context hierarchies and converting to/from string representations.
9+
class StripeContext
10+
include Comparable
11+
12+
attr_reader :segments
13+
14+
# Creates a new StripeContext with the given segments.
15+
def initialize(segments = nil)
16+
@segments = (segments || []).map(&:to_s).freeze
17+
end
18+
19+
# Parses a context string into a StripeContext instance.
20+
def self.parse(context_str)
21+
return new if context_str.nil? || context_str.empty?
22+
23+
new(context_str.split("/"))
24+
end
25+
26+
# Creates a new StripeContext with an additional segment appended.
27+
def push(segment)
28+
segment_str = segment.to_s.strip
29+
raise ArgumentError, "Segment cannot be empty or whitespace" if segment_str.empty?
30+
31+
new_segments = @segments + [segment_str]
32+
self.class.new(new_segments)
33+
end
34+
35+
# Creates a new StripeContext with the last segment removed.
36+
# If there are no segments, returns a new empty StripeContext.
37+
def pop
38+
raise IndexError, "No segments to pop" if @segments.empty?
39+
40+
new_segments = @segments[0...-1]
41+
self.class.new(new_segments)
42+
end
43+
44+
# Converts this context to its string representation.
45+
def to_s
46+
@segments.join("/")
47+
end
48+
49+
# Checks equality with another StripeContext.
50+
def ==(other)
51+
other.is_a?(StripeContext) && @segments == other.segments
52+
end
53+
54+
# Alias for == to support eql? method
55+
alias eql? ==
56+
57+
# Returns a human-readable representation for debugging.
58+
def inspect
59+
"#<#{self.class}:0x#{object_id.to_s(16)} segments=#{@segments.inspect}>"
60+
end
61+
62+
# Returns true if the context has no segments.
63+
# @return [Boolean] true if empty, false otherwise
64+
def empty?
65+
@segments.empty?
66+
end
67+
end
68+
end

0 commit comments

Comments
 (0)