spackle-ruby 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da95d997ac3fdc1de27078762389d267be3d9ff4a89091147eef7d8067a71fd1
4
- data.tar.gz: 064c9007ff68c9f51003c49b31ff467595511acc876dd90cd5916ab5ea9d2c2d
3
+ metadata.gz: e8cf2b94103517a295249a12015de47bba73211680e6414e171df2bc9ab18ceb
4
+ data.tar.gz: 694bb847e82273d30bb5f7e325bdb795e56a5d4555bcefe01783dc597d22c3d6
5
5
  SHA512:
6
- metadata.gz: c70087975948d0a24ae44f79ade37f0f821035e1503576992ed82b71dc81733547e22d87106f38e8bed5b48265194402f8f50b0d1968b5d05f41792e7196c6a6
7
- data.tar.gz: 1b60e61ecca69ce71662c9a95a1d6b45ef24a4fac3d5469fcf4c21ab74818a63386adfa8bd6d14564016454874d38d1f2013881859dbd55a73a725497210076f
6
+ metadata.gz: e8e7b93be2dc27f5b823741bed3c29b05113811fb37ae6769e89cf323eb5f6b34c2f6aa1d402e15b064dae86e6864ac155cf6f3fb565fa3c873107020119384c
7
+ data.tar.gz: 0e4f7073e81f0bba5a572f9ff5e1ad9670cbf0f5e5ec9afc93d3de20c2c2d0cfe476208a232403aaa61046abed6f1e826c3b98fdd9e373bff61d0410067fcbb7
data/Gemfile CHANGED
@@ -4,8 +4,9 @@ source "https://rubygems.org"
4
4
 
5
5
  git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
6
6
 
7
- gem 'aws-sdk', '~> 3'
7
+ gem "aws-sdk", "~> 3"
8
8
  gem "rake", "~> 13.0"
9
- gem "test-unit", "~> 3.5"
10
-
11
9
  gem "nokogiri", "~> 1.13"
10
+ gem "minitest", "~> 5.18"
11
+
12
+ gem "stripe", "~> 8.3"
data/Gemfile.lock CHANGED
@@ -1356,23 +1356,23 @@ GEM
1356
1356
  aws-eventstream (~> 1, >= 1.0.2)
1357
1357
  jmespath (1.6.2)
1358
1358
  mini_portile2 (2.8.1)
1359
+ minitest (5.18.0)
1359
1360
  nokogiri (1.13.10)
1360
1361
  mini_portile2 (~> 2.8.0)
1361
1362
  racc (~> 1.4)
1362
- power_assert (2.0.3)
1363
1363
  racc (1.6.2)
1364
1364
  rake (13.0.6)
1365
- test-unit (3.5.7)
1366
- power_assert
1365
+ stripe (8.3.0)
1367
1366
 
1368
1367
  PLATFORMS
1369
1368
  ruby
1370
1369
 
1371
1370
  DEPENDENCIES
1372
1371
  aws-sdk (~> 3)
1372
+ minitest (~> 5.18)
1373
1373
  nokogiri (~> 1.13)
1374
1374
  rake (~> 13.0)
1375
- test-unit (~> 3.5)
1375
+ stripe (~> 8.3)
1376
1376
 
1377
1377
  BUNDLED WITH
1378
1378
  1.17.2
data/README.md CHANGED
@@ -21,7 +21,7 @@ gem install spackle-ruby
21
21
  ```ruby
22
22
  source 'https://rubygems.org'
23
23
 
24
- gem 'spackle'
24
+ gem 'spackle-ruby'
25
25
  ```
26
26
 
27
27
  ### Configure your environment
@@ -63,10 +63,37 @@ customer.enabled("feature_key")
63
63
  customer.limit("feature_key")
64
64
  ```
65
65
 
66
+ ### Examine a customer's subscriptions
67
+
68
+ A customer's current subscriptions are available on the `subscriptions` property. These are valid `Stripe::Subscription` objects as defined in the [Stripe Ruby library](https://stripe.com/docs/api/subscriptions/object?lang=ruby).
69
+
70
+ ```ruby
71
+ customer.subscriptions
72
+ ```
73
+
74
+ ## Waiters
75
+
76
+ There is a brief delay between when an action takes place in Stripe and when it is reflected in Spackle. To account for this, Spackle provides a `Waiters` module that can be used to wait for a Stripe object to be updated and replicated.
77
+
78
+ 1. Wait for a customer to be created
79
+ ```ruby
80
+ Spackle::Waiters.wait_for_customer("cus_00000000")
81
+ ```
82
+ 2. Wait for a subscription to be created
83
+ ```ruby
84
+ Spackle::Waiters.wait_for_subscription("cus_000000000", "sub_00000000")
85
+ ```
86
+ 3. Wait for a subscription to be updated
87
+ ```ruby
88
+ Spackle::Waiters.wait_for_subscription("cus_000000000", "sub_00000000", status: "active")
89
+ ```
90
+
91
+ These will block until Spackle is updated with the latest information from Stripe or until a timeout occurs.
92
+
66
93
  ## Logging
67
94
  The Spackle Ruby library emits logs as it performs various internal tasks. You can control the verbosity of Spackle's logging a few different ways:
68
95
 
69
- 1. Set the environment variable SPACKLE_LOG to the value `debug`, `info`, or `error`
96
+ 1. Set the environment variable SPACKLE_LOG to the value `debug`, `info`, `warn` or `error`
70
97
 
71
98
  ```sh
72
99
  $ export SPACKLE_LOG=debug
@@ -77,3 +104,67 @@ The Spackle Ruby library emits logs as it performs various internal tasks. You c
77
104
  ```ruby
78
105
  Spackle.log_level = 'debug'
79
106
  ```
107
+
108
+ ## Usage in development environments
109
+ In production, Spackle requires a valid Stripe customer. However, that is not development environments where state needs to be controlled. As an alternative, you can use a file store to test your application with seed data.
110
+
111
+ ```json
112
+ /app/spackle.json
113
+
114
+ {
115
+ "cus_000000000": {
116
+ "features": [
117
+ {
118
+ "key": "flag_feature",
119
+ "value_flag": true
120
+ },
121
+ {
122
+ "key": "limit_feature",
123
+ "value_limit": 100
124
+ }
125
+ ],
126
+ "subscriptions": [
127
+ {
128
+ "id": "sub_000000000",
129
+ "status": "trialing",
130
+ "quantity": 1
131
+ }
132
+ ]
133
+ }
134
+ }
135
+ ```
136
+
137
+ Then configure the file store in your application:
138
+
139
+ ```ruby
140
+ Spackle.store = Spackle::FileStore.new('/app/spackle.json')
141
+ ```
142
+
143
+ ## Usage in test environments
144
+
145
+ In production, Spackle requires a valid Stripe customer. However, that is not ideal in testing or some development environments. As an alternative, you can use an in-memory store to test your application with seed data.
146
+
147
+ ```ruby
148
+ Spackle.store = Spackle::MemoryStore.new()
149
+ Spackle.store.set_customer_data("cus_000000000", {
150
+ "features": [
151
+ {
152
+ "key": "flag_feature",
153
+ "value_flag": True,
154
+ },
155
+ {
156
+ "key": "limit_feature",
157
+ "value_limit": 100,
158
+ },
159
+ ],
160
+ "subscriptions": [
161
+ {
162
+ "id": "sub_000000000",
163
+ "status": "trialing",
164
+ "quantity": 1,
165
+ }
166
+ ]
167
+ })
168
+ ```
169
+
170
+ **Note:** The in-memory store is not thread-safe and state will reset on each application restart.
data/Rakefile CHANGED
@@ -1,7 +1,14 @@
1
- require "rake/testtask"
1
+ require "minitest/test_task"
2
2
 
3
- task default: %i[test]
3
+ Minitest::TestTask.create # named test, sensible defaults
4
4
 
5
- Rake::TestTask.new do |t|
6
- t.pattern = "./test/**/*_test.rb"
5
+ # or more explicitly:
6
+
7
+ Minitest::TestTask.create(:test) do |t|
8
+ t.libs << "test"
9
+ t.libs << "lib"
10
+ t.warning = false
11
+ t.test_globs = ["test/**/*_test.rb"]
7
12
  end
13
+
14
+ task :default => :test
@@ -1,38 +1,64 @@
1
+ require 'stripe'
2
+
1
3
  module Spackle
2
4
  class Customer
3
- @data = nil
5
+ attr_accessor :id
6
+ attr_accessor :data
4
7
 
5
8
  def self.retrieve(id)
6
9
  Util.log_debug("Retrieving customer data for #{id}")
7
- data = Spackle.client.get_item({
8
- 'CustomerId' => id
9
- })
10
+ data = Spackle.store.get_customer_data(id)
10
11
  Util.log_debug("Retrieved customer data for #{id}: #{data}")
11
- Customer.new(data)
12
+ Customer.new(id, data)
12
13
  end
13
14
 
14
- def initialize(data)
15
+ def initialize(id, data)
16
+ @id = id
15
17
  @data = data
16
18
  end
17
19
 
20
+ def features
21
+ return @data['features']
22
+ end
23
+
24
+ def subscriptions
25
+ subscriptions = []
26
+
27
+ @data['subscriptions'].each do |s|
28
+ subscription = Stripe::Subscription.new(s['id'])
29
+ subscription.update_attributes(s)
30
+ subscriptions.push(subscription)
31
+ end
32
+
33
+ subscriptions
34
+ end
35
+
36
+ def flag_features
37
+ features.select { |f| f['type'] == 0 }
38
+ end
39
+
40
+ def limit_features
41
+ features.select { |f| f['type'] == 1 }
42
+ end
43
+
18
44
  def enabled(key)
19
- @data['features'].each do |f|
45
+ flag_features.each do |f|
20
46
  if f['key'] == key
21
47
  return f['value_flag']
22
48
  end
23
49
  end
24
50
 
25
- return false
51
+ raise SpackleError.new "Flag feature #{key} not found"
26
52
  end
27
53
 
28
54
  def limit(key)
29
55
  @data['features'].each do |f|
30
56
  if f['key'] == key
31
- return f['value_limit']
57
+ return f['value_limit'] || Float::INFINITY
32
58
  end
33
59
  end
34
60
 
35
- return 0
61
+ raise SpackleError.new "Limit feature #{key} not found"
36
62
  end
37
63
  end
38
64
  end
@@ -4,18 +4,35 @@ module Spackle
4
4
  class SpackleConfiguration
5
5
  attr_accessor :api_key
6
6
  attr_accessor :api_base
7
- attr_accessor :log_level
8
7
  attr_reader :logger
8
+ attr_reader :version
9
9
 
10
10
  def initialize
11
- @api_base = 'https://api.spackle.so'
12
- @log_level = Logger::INFO
11
+ @api_base = 'https://api.spackle.so/v1'
12
+ @log_level = Logger::WARN
13
13
  @logger = Logger.new(STDOUT, level: @log_level)
14
+ @version = 1
15
+ @store = nil
16
+ end
17
+
18
+ def log_level()
19
+ @log_level
14
20
  end
15
21
 
16
22
  def log_level=(level)
17
23
  @log_level = level
18
24
  @logger.level = level
19
25
  end
26
+
27
+ def store()
28
+ if @store == nil
29
+ @store = DynamoDBStore.new
30
+ end
31
+ @store
32
+ end
33
+
34
+ def store=(store)
35
+ @store = store
36
+ end
20
37
  end
21
38
  end
@@ -0,0 +1,11 @@
1
+ module Spackle
2
+ class BaseStore
3
+ def get_customer_data(id)
4
+ raise NoMethodError, 'get_customer_data not implemented'
5
+ end
6
+
7
+ def set_customer_data(id, data)
8
+ raise NoMethodError, 'set_customer_data not implemented'
9
+ end
10
+ end
11
+ end
@@ -1,21 +1,41 @@
1
1
  require 'aws-sdk'
2
2
  require 'net/http'
3
3
  require 'json'
4
+ require 'logger'
4
5
 
5
6
  module Spackle
6
- class DynamoDB
7
+ class DynamoDBStore < BaseStore
7
8
  @client = nil
8
- @identity_id = nil
9
- @table_name = nil
10
- @aws_region = nil
9
+ @store_config = {}
11
10
 
12
- def initialize
13
- @client = bootstrap_client
11
+ def initialize(client = nil, store_config = nil)
12
+ @store_config = store_config || {}
13
+ @client = client || bootstrap_client
14
14
  end
15
15
 
16
+ def get_customer_data(id)
17
+ data = query({
18
+ key_condition_expression: 'CustomerId = :customer_id',
19
+ filter_expression: 'Version = :version',
20
+ expression_attribute_values: {
21
+ ':customer_id' => id,
22
+ ':version' => Spackle.version
23
+ },
24
+ limit: 1
25
+ })
26
+
27
+ if not data.items.any?
28
+ raise SpackleError.new "Customer #{id} not found"
29
+ end
30
+
31
+ JSON.parse(data.items[0]['State'])
32
+ end
33
+
34
+ private
35
+
16
36
  def get_item(key)
17
37
  key = key.merge({
18
- 'AccountId' => @identity_id,
38
+ 'AccountId' => @store_config['identity_id'],
19
39
  })
20
40
 
21
41
  response = @client.get_item({
@@ -26,12 +46,18 @@ module Spackle
26
46
  JSON.parse(response.item['State'])
27
47
  end
28
48
 
29
- private
30
-
49
+ def query(query)
50
+ query[:table_name] = @store_config['table_name']
51
+ query[:key_condition_expression] = 'AccountId = :account_id AND ' + query[:key_condition_expression]
52
+ query[:expression_attribute_values] = query[:expression_attribute_values].merge({
53
+ ':account_id' => @store_config['identity_id']
54
+ })
55
+ @client.query(query)
56
+ end
31
57
 
32
58
  def bootstrap_client
33
59
  Util.log_debug('Bootstrapping DynamoDB client...')
34
- uri = URI(Spackle.api_base + '/auth/session')
60
+ uri = URI(Spackle.api_base + '/sessions')
35
61
  https = Net::HTTP.new(uri.host, uri.port)
36
62
  https.use_ssl = true
37
63
 
@@ -42,17 +68,13 @@ module Spackle
42
68
  data = JSON.parse(response.body)
43
69
  Util.log_debug("Created session: #{data}")
44
70
 
45
- @identity_id = data['identity_id']
46
- @table_name = data['table_name']
47
- @aws_region = data['aws_region']
48
-
71
+ @store_config = data['adapter']
49
72
  credentials = SpackleCredentials.new(
50
- data['role_arn'],
51
- data['token']
73
+ @store_config['role_arn'],
74
+ @store_config['token']
52
75
  )
53
76
 
54
77
  Aws::DynamoDB::Client.new(
55
- region: @aws_region,
56
78
  credentials: credentials,
57
79
  )
58
80
  end
@@ -0,0 +1,28 @@
1
+ module Spackle
2
+ class FileStore < BaseStore
3
+ def initialize(path)
4
+ @path = path
5
+ end
6
+
7
+ def get_customer_data(id)
8
+ content = File.read(@path)
9
+ data = JSON.parse(content)
10
+ data[id]
11
+ end
12
+
13
+ def set_customer_data(id, customer_data)
14
+ data = {}
15
+ if File.exist?(@path)
16
+ content = File.read(@path)
17
+ data = JSON.parse(content)
18
+ end
19
+
20
+ if !data.has_key?(id)
21
+ raise SpackleError.new "Customer #{id} not found"
22
+ end
23
+
24
+ data[id] = customer_data
25
+ File.write(@path, JSON.pretty_generate(data))
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ module Spackle
2
+ class MemoryStore < BaseStore
3
+ @data = {}
4
+
5
+ def initialize()
6
+ @data = {}
7
+ end
8
+
9
+ def get_customer_data(id)
10
+ if !@data.has_key?(id)
11
+ raise SpackleError.new "Customer #{id} not found"
12
+ end
13
+
14
+ @data[id]
15
+ end
16
+
17
+ def set_customer_data(id, customer_data)
18
+ @data[id] = customer_data
19
+ end
20
+ end
21
+ end
data/lib/spackle/util.rb CHANGED
@@ -1,15 +1,21 @@
1
+ require 'logger'
2
+
1
3
  module Spackle
2
4
  module Util
3
- def self.log_error(message)
4
- Spackle.logger.error(message)
5
+ def self.log_debug(message)
6
+ Spackle.logger.debug(message)
5
7
  end
6
8
 
7
9
  def self.log_info(message)
8
10
  Spackle.logger.info(message)
9
11
  end
10
12
 
11
- def self.log_debug(message)
12
- Spackle.logger.debug(message)
13
+ def self.log_warn(message)
14
+ Spackle.logger.warn(message)
15
+ end
16
+
17
+ def self.log_error(message)
18
+ Spackle.logger.error(message)
13
19
  end
14
20
  end
15
21
  end
@@ -0,0 +1,37 @@
1
+ require 'logger'
2
+
3
+ module Spackle
4
+ module Waiters
5
+ def self.wait_for_customer(customer_id, timeout = 15)
6
+ start = Time.now.to_i
7
+ while Time.now.to_i - start < timeout do
8
+ begin
9
+ return Spackle::Customer.retrieve(customer_id)
10
+ rescue SpackleError
11
+ sleep 1
12
+ end
13
+ end
14
+
15
+ raise SpackleError.new "Timeout waiting for customer #{customer_id}"
16
+ end
17
+
18
+ def self.wait_for_subscription(customer_id, subscription_id, timeout=15, **filters)
19
+ start = Time.now.to_i
20
+ while Time.now.to_i - start < timeout do
21
+ begin
22
+ customer = Spackle::Customer.retrieve(customer_id)
23
+ customer.subscriptions.each do |s|
24
+ if s.id == subscription_id and filters.all? { |k, v| s.send(k) == v }
25
+ return s
26
+ end
27
+ end
28
+ sleep 1
29
+ rescue SpackleError
30
+ sleep 1
31
+ end
32
+ end
33
+
34
+ raise SpackleError.new "Timeout waiting for subscription #{subscription_id}"
35
+ end
36
+ end
37
+ end
data/lib/spackle.rb CHANGED
@@ -2,17 +2,23 @@ require 'logger'
2
2
  require 'forwardable'
3
3
 
4
4
  require 'spackle/customer'
5
- require 'spackle/dynamodb'
6
5
  require 'spackle/spackle_configuration'
7
6
  require 'spackle/util'
7
+ require 'spackle/waiters'
8
+
9
+ require 'spackle/stores/base'
10
+ require 'spackle/stores/dynamodb'
11
+ require 'spackle/stores/file'
12
+ require 'spackle/stores/memory'
8
13
 
9
14
  module Spackle
10
15
  @config = Spackle::SpackleConfiguration.new
11
16
  @client = nil
12
17
 
13
18
  LEVEL_DEBUG = Logger::DEBUG
14
- LEVEL_ERROR = Logger::ERROR
15
19
  LEVEL_INFO = Logger::INFO
20
+ LEVEL_WARN = Logger::WARN
21
+ LEVEL_ERROR = Logger::ERROR
16
22
 
17
23
  class << self
18
24
  extend Forwardable
@@ -22,18 +28,12 @@ module Spackle
22
28
  def_delegators :@config, :api_key, :api_key=
23
29
  def_delegators :@config, :api_base, :api_base=
24
30
  def_delegators :@config, :log_level, :log_level=
31
+ def_delegators :@config, :store, :store=
25
32
  def_delegators :@config, :logger, :logger=
33
+ def_delegators :@config, :version, :version=
26
34
  end
27
35
 
28
- def self.client
29
- unless Spackle.api_key.nil?
30
- @client ||= Spackle::DynamoDB.new()
31
- end
32
- end
33
-
34
- def self.bootstrap
35
- self.client
36
- nil
36
+ class SpackleError < StandardError
37
37
  end
38
38
  end
39
39
 
data/spackle.gemspec CHANGED
@@ -2,9 +2,9 @@ $LOAD_PATH.unshift(::File.join(::File.dirname(__FILE__), "lib"))
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "spackle-ruby"
5
- s.version = "0.0.1"
5
+ s.version = "0.0.3"
6
6
  s.summary = "Spackle Ruby gem"
7
- s.description = "Spackle is the easiest way to integrate your Ruby app with Stripe Billing." \
7
+ s.description = "Spackle is the easiest way to integrate your Ruby app with Stripe Billing. " \
8
8
  "See https://www.spackle.so for details."
9
9
  s.authors = ["Spackle"]
10
10
  s.email = "support@spackle.so"
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spackle-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Spackle
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-08 00:00:00.000000000 Z
11
+ date: 2023-03-06 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Spackle is the easiest way to integrate your Ruby app with Stripe Billing.See
14
- https://www.spackle.so for details.
13
+ description: Spackle is the easiest way to integrate your Ruby app with Stripe Billing.
14
+ See https://www.spackle.so for details.
15
15
  email: support@spackle.so
16
16
  executables:
17
17
  - spackle-console
@@ -26,9 +26,13 @@ files:
26
26
  - bin/spackle-console
27
27
  - lib/spackle.rb
28
28
  - lib/spackle/customer.rb
29
- - lib/spackle/dynamodb.rb
30
29
  - lib/spackle/spackle_configuration.rb
30
+ - lib/spackle/stores/base.rb
31
+ - lib/spackle/stores/dynamodb.rb
32
+ - lib/spackle/stores/file.rb
33
+ - lib/spackle/stores/memory.rb
31
34
  - lib/spackle/util.rb
35
+ - lib/spackle/waiters.rb
32
36
  - spackle.gemspec
33
37
  homepage: https://docs.spackle.so/ruby
34
38
  licenses: