big_commerce-management_api 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 633a92516ac21a7df777b1bdae97d4b0ec623f00f0946b77822bf04432cde707
4
+ data.tar.gz: 3409875235a0efe188b7a9f16fe22040600546dd68a5fe7ed739547934a4fb9c
5
+ SHA512:
6
+ metadata.gz: fa94744067607cd0e15129a0815c2a45465ddbdd3b3dece4adb637494758b250d77febac896b23d5431db258843edb613b389743471f15bb6c2ed4939a27f5a9
7
+ data.tar.gz: fec8d7979001e9193e76d10c0231be65ec84312828969fd76a649beb453bf7fc49633c9332b4e9f454ae32d49c82ac58bb65a1c75336e2bd703ef35b25a69793
data/.env.test.example ADDED
@@ -0,0 +1,3 @@
1
+ # Used by VCR-based tests
2
+ BC_ACCESS_TOKEN=
3
+ BC_STORE_HASH=
@@ -0,0 +1,24 @@
1
+ name: CI
2
+
3
+ on:
4
+ - push
5
+ - pull_request
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ env:
11
+ BC_ACCESS_TOKEN: "Foo"
12
+ BC_STORE_HASH: "Bar"
13
+ strategy:
14
+ matrix:
15
+ ruby: ['3.2', '3.1', '3.0', '2.7', '2.6', '2.5']
16
+
17
+ steps:
18
+ - uses: actions/checkout@v3
19
+ - uses: ruby/setup-ruby@v1
20
+ with:
21
+ ruby-version: ${{ matrix.ruby }}
22
+ bundler-cache: true
23
+
24
+ - run: bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,62 @@
1
+ # Created by https://www.toptal.com/developers/gitignore/api/ruby
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=ruby
3
+
4
+ ### Ruby ###
5
+ *.gem
6
+ *.rbc
7
+ /.config
8
+ /coverage/
9
+ /InstalledFiles
10
+ /pkg/
11
+ /spec/reports/
12
+ /spec/examples.txt
13
+ /test/tmp/
14
+ /test/version_tmp/
15
+ /tmp/
16
+
17
+ # Used by dotenv library to load environment variables.
18
+ .env*
19
+ !.env*.example
20
+
21
+ # Ignore Byebug command history file.
22
+ .byebug_history
23
+
24
+ ## Specific to RubyMotion:
25
+ .dat*
26
+ .repl_history
27
+ build/
28
+ *.bridgesupport
29
+ build-iPhoneOS/
30
+ build-iPhoneSimulator/
31
+
32
+ ## Specific to RubyMotion (use of CocoaPods):
33
+ #
34
+ # We recommend against adding the Pods directory to your .gitignore. However
35
+ # you should judge for yourself, the pros and cons are mentioned at:
36
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
37
+ # vendor/Pods/
38
+
39
+ ## Documentation cache and generated files:
40
+ /.yardoc/
41
+ /_yardoc/
42
+ /doc/
43
+ /rdoc/
44
+
45
+ ## Environment normalization:
46
+ /.bundle/
47
+ /vendor/bundle
48
+ /lib/bundler/man/
49
+
50
+ # for a library or gem, you might want to ignore these files since the code is
51
+ # intended to run in multiple environments; otherwise, check them in:
52
+ Gemfile.lock
53
+ .ruby-version
54
+ .ruby-gemset
55
+
56
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
57
+ .rvmrc
58
+
59
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
60
+ # .rubocop-https?--*
61
+
62
+ # End of https://www.toptal.com/developers/gitignore/api/ruby
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in big_commerce_rest.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "rspec", "~> 3.0"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Skye Shaw
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # BigCommerce::ManagementAPI
2
+
3
+ ![CI Status](https://github.com/ScreenStaring/big_commerce-management_api/actions/workflows/ci.yml/badge.svg)
4
+
5
+ v3 API client for [BigCommerce's REST Management API](https://developer.bigcommerce.com/docs/rest-management)
6
+
7
+ **Incomplete! v3 has many endpoints and this only provides what we need at ScreenStaring** which currently
8
+ is mostly customers and subscribers stuff but adding new endpoints should be trivial. See [Adding New Endpoints](#adding-new-endpoints).
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's `Gemfile`:
13
+
14
+ ```ruby
15
+ gem "big_commerce-management_api"
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ bundle install
21
+
22
+ Or install it yourself as:
23
+
24
+ gem install big_commerce-management_api
25
+
26
+ ## Usage
27
+
28
+ ```rb
29
+ require "big_commerce/management_api"
30
+
31
+ bc = BigCommerce::ManagementAPI.new(store_hash, auth_token)
32
+ customers = bc.customers.get
33
+ customers = bc.customers.get(:id => [1,2,3], :include => %w[addresses formfields], :page => 10, :limit => 25)
34
+
35
+ p customers.meta.pagination.total
36
+ p customers.headers.request_id
37
+ p customers.headers["some-header"]
38
+
39
+ # customers is Enumerable
40
+ customers.each do |customer|
41
+ p customer.first_name
42
+ p customer.addresses[0].address_type
43
+ # ...
44
+ end
45
+
46
+ begin
47
+ customers = bc.customers.get(:page => 1, :size => 99)
48
+ rescue BigCommerce::ManagementAPI::ResponseError => e
49
+ p e.message
50
+ p e.headers.rate_limit_requests_left
51
+ end
52
+
53
+ attribute = bc.customers.attributes.create(
54
+ :name => "Daily screen-staring count",
55
+ :type => "number"
56
+ )
57
+ p attribute.id
58
+ p attribute.meta.total # 1
59
+ ```
60
+
61
+ ## Adding New Endpoints
62
+
63
+ 1. Add JSON response body to `lib/big_commerce/management/classes.json`
64
+ 1. Create or update `lib/big_commerce/management/THE_RESOURCE.rb`. See `customers.rb` for an example
65
+ 1. For new classes
66
+ - If the response JSON's top-level property is not `"data"` define `RESULT_KEY` with the name of the top-level property
67
+ - Define `RESULT_INSTANCE` and set it to the class to use on response data pointed to by `RESULT_KEY`
68
+ - Call the appropriate HTTP verb method passing the endpoint's path (the portion **after** the API's `v3` URL) and parameters
69
+ 1. If the method's return value should not be an `Array` call `unwrap(result)` before returning
70
+
71
+ ## Testing
72
+
73
+ Tests use [VCR](https://github.com/vcr/vcr). If you need to re-record cassettes or create new ones a BigCommerce
74
+ account is with API access is required. See `.env.test.example`.
75
+
76
+ To re-record certain tests you must import fixture data into your store. See `etc/customers.csv`. These records can be deleted once
77
+ the VCR cassettes are recorded and you are done with development. The IDs they create are assumed by the tests which may present
78
+ a problem. Open an issue if so.
79
+
80
+ Any records that are created by the tests are deleted. Well, a delete is attempted in an `after` block, if something goes wrong with the test
81
+ the record(s) may remain in your store.
82
+
83
+ ## License
84
+
85
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
86
+
87
+ ---
88
+
89
+ Made by [ScreenStaring](http://screenstaring.com)
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,33 @@
1
+ require_relative 'lib/big_commerce/management_api/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "big_commerce-management_api"
5
+ spec.version = BigCommerce::ManagementAPI::VERSION
6
+ spec.authors = ["Skye Shaw"]
7
+ spec.email = ["skye.shaw@gmail.com"]
8
+
9
+ spec.summary = %q{v3 API client for BigCommerce's REST Management API}
10
+ spec.description = %q{v3 API client for BigCommerce's REST Management API. Implementation is far from complete but adding support for new endpoints is trivial.}
11
+ spec.homepage = "https://github.com/ScreenStaring/big_commerce-management_api"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = "https://github.com/ScreenStaring/big_commerce-management_api"
17
+ spec.metadata["bug_tracker_uri"] = "https://github.com/ScreenStaring/big_commerce-management_api/issues"
18
+ # spec.metadata["changelog_uri"] = "https://github.com/ScreenStaring/big_commerce-management_api/blob/master/Changes"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_dependency "class2", ">= 0.6.0"
30
+ spec.add_development_dependency "vcr"
31
+ spec.add_development_dependency "webmock"
32
+ spec.add_development_dependency "dotenv"
33
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "big_commerce/management_api"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/etc/customers.csv ADDED
@@ -0,0 +1,6 @@
1
+ Email Address,First Name,Last Name,Company,Phone,Notes,Store Credit,Customer Group,Address ID - 1,Address First Name - 1,Address Last Name - 1,Address Company - 1,Address Line 1 - 1,Address Line 2 - 1,Address City - 1,Address State - 1,Address Zip - 1,Address Country - 1,Receive Review/Abandoned Cart Emails?
2
+ user1@example.com,John,Doe,Acme Inc. ,555-555-1212,Note 1,59.99,,,,,Fofiha,59 West 46th Street,,New York,New York,10005,United States,N
3
+ user2@example.com,Bob,John,Acme Inc. ,212-555-1212,,,,,,,,123 55th St. ,#1,New York,New York,10038,United States,N
4
+ user3@example.com,Richard,Smith,FooBar LLC,310-555-1212,,101,,,Abe,Simpson,,21 W. 21st St. ,,New York,New York,10015,United States,Y
5
+ user4@example.com,Paulo,Costa,,,,,,,,,,666 6th Ave. ,,New York,New York,10005,United States,N
6
+ user5@example.com,Caio,Oliver ,,510-555-1212,,,,,,,Fofiha,125 55th St. ,Apt 3Q,New York,New York,10001,United States,Y
@@ -0,0 +1,130 @@
1
+ {
2
+ "attribute": {
3
+ "id": 1,
4
+ "name": "Age",
5
+ "type": "string",
6
+ "date_modified": {"json_class":"Time","s":0,"n":0},
7
+ "date_created": {"json_class":"Time","s":0,"n":0}
8
+ },
9
+ "attribute_value": {
10
+ "attribute_id": 0,
11
+ "attribute_value": "string",
12
+ "id": 0,
13
+ "customer_id": 0,
14
+ "date_modified": {"json_class":"Time","s":0,"n":0},
15
+ "date_created": {"json_class":"Time","s":0,"n":0}
16
+ },
17
+ "customer": {
18
+ "id": 1,
19
+ "email": "string@example.com",
20
+ "first_name": "string",
21
+ "last_name": "string",
22
+ "company": "string",
23
+ "phone": "string",
24
+ "notes": "string",
25
+ "tax_exempt_category": "string",
26
+ "customer_group_id": 0,
27
+ "addresses": [
28
+ {
29
+ "first_name": "string",
30
+ "last_name": "string",
31
+ "company": "string",
32
+ "address1": "Addr1",
33
+ "address2": "",
34
+ "city": "string",
35
+ "state_or_province": "string",
36
+ "postal_code": "string",
37
+ "country_code": "st",
38
+ "phone": "string",
39
+ "address_type": "residential",
40
+ "customer_id": 0,
41
+ "id": 0,
42
+ "country": "string"
43
+ }
44
+ ],
45
+ "store_credit_amounts": [
46
+ {
47
+ "amount": 43.15
48
+ }
49
+ ],
50
+ "form_fields": [
51
+ { "name": "string", "value": "string" }
52
+ ],
53
+ "accepts_product_review_abandoned_cart_emails": true,
54
+ "channel_ids": [],
55
+ "shopper_profile_id": "82511e54-4040-40fe-b742-2b25655f205b",
56
+ "segment_ids": [],
57
+ "date_modified": {"json_class":"Time","s":0,"n":0},
58
+ "date_created": {"json_class":"Time","s":0,"n":0}
59
+ },
60
+ "metafield": {
61
+ "id": 0,
62
+ "owner_client_id": "X123",
63
+ "key": "Staff Name",
64
+ "value": "Ronaldo",
65
+ "namespace": "Sales Department",
66
+ "permission_set": "app_only",
67
+ "resource_type": "cart",
68
+ "resource_id": 1,
69
+ "description": "order",
70
+ "date_modified": {"json_class":"Time","s":0,"n":0},
71
+ "date_created": {"json_class":"Time","s":0,"n":0}
72
+ },
73
+ "inventory": {
74
+ "identity": {
75
+ "sku": "RE-130",
76
+ "variant_id": 79,
77
+ "product_id": 120,
78
+ "sku_id": 0
79
+ },
80
+ "locations": [
81
+ {
82
+ "location_id": 1,
83
+ "location_code": "BC-LOCATION-1",
84
+ "location_name": "Default location",
85
+ "available_to_sell": 10,
86
+ "total_inventory_onhand": 11,
87
+ "location_enabled": true,
88
+ "settings": {
89
+ "safety_stock": 1,
90
+ "is_in_stock": true,
91
+ "warning_level": 1,
92
+ "bin_picking_number": "1"
93
+ }
94
+ }
95
+ ]
96
+ },
97
+ "segment": {
98
+ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
99
+ "name": "My Segment",
100
+ "description": "Description",
101
+ "updated_at": {"json_class":"Time","s":0,"n":0},
102
+ "created_at": {"json_class":"Time","s":0,"n":0}
103
+ },
104
+ "subscriber": {
105
+ "email": "string",
106
+ "first_name": "string",
107
+ "last_name": "string",
108
+ "source": "string",
109
+ "order_id": 1,
110
+ "channel_id": 1,
111
+ "id": 0,
112
+ "date_modified": {"json_class":"Time","s":0,"n":0},
113
+ "date_created": {"json_class":"Time","s":0,"n":0},
114
+ "consents": []
115
+ },
116
+ "meta": {
117
+ "pagination": {
118
+ "total": 246,
119
+ "count": 5,
120
+ "per_page": 5,
121
+ "current_page": 1,
122
+ "total_pages": 50,
123
+ "links": {
124
+ "previous": "?limit=5&page=2",
125
+ "current": "?limit=5&page=3",
126
+ "next": "?limit=5&page=4"
127
+ }
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "class2"
4
+ require "json"
5
+
6
+ # This results in certain properties in the JSON samples/definitions to be parsed as
7
+ # Time which class2 will see and convert the attribute values to Time
8
+ require "json/add/time"
9
+
10
+ classes = File.read(File.join(__dir__, "classes.json"))
11
+ class2 "BigCommerce::ManagementAPI", JSON.parse(classes, :create_additions => true) do
12
+ def meta
13
+ @meta
14
+ end
15
+
16
+ def meta=(meta)
17
+ @meta = meta
18
+ end
19
+
20
+ def headers
21
+ @headers
22
+ end
23
+
24
+ def headers=(headers)
25
+ @headers = headers
26
+ end
27
+ end
28
+
29
+ require "big_commerce/management_api/customers"
30
+ require "big_commerce/management_api/inventories"
31
+ require "big_commerce/management_api/segments"
32
+ require "big_commerce/management_api/subscribers"
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "big_commerce/management_api/endpoint"
4
+
5
+ module BigCommerce
6
+ module ManagementAPI
7
+ class Customers < Endpoint
8
+ class Addresses < Endpoint
9
+ PATH = "customers/addresses"
10
+ RESULT_INSTANCE = Address
11
+
12
+ def create(*attributes)
13
+ attributes.flatten!
14
+
15
+ POST(PATH, attributes.map(&:to_h))
16
+ end
17
+
18
+ def delete(*ids)
19
+ ids.flatten!
20
+
21
+ DELETE(
22
+ PATH,
23
+ with_in_param({:id => ids}, :id)
24
+ )
25
+ end
26
+
27
+ def get(options = {})
28
+ GET(
29
+ PATH,
30
+ with_in_param(
31
+ options,
32
+ :company,
33
+ :customer_id,
34
+ :id,
35
+ :name
36
+ )
37
+ )
38
+ end
39
+ end
40
+
41
+ class Attributes < Endpoint
42
+ PATH = "customers/attributes"
43
+ RESULT_INSTANCE = Attribute
44
+
45
+ def get(options = {})
46
+ GET(PATH, options)
47
+ end
48
+
49
+ def create(*attributes)
50
+ attributes.flatten!
51
+
52
+ POST(PATH, attributes.map(&:to_h))
53
+ end
54
+
55
+ def delete(*ids)
56
+ ids.flatten!
57
+
58
+ DELETE(
59
+ PATH,
60
+ with_in_param({:id => ids}, :id)
61
+ )
62
+ end
63
+ end
64
+
65
+ class AttributeValues < Endpoint
66
+ PATH = "customers/attribute-values"
67
+ RESULT_INSTANCE = AttributeValue
68
+
69
+ def get(options = {})
70
+ GET(
71
+ PATH,
72
+ with_in_param(
73
+ options,
74
+ :attribute_id,
75
+ :customer_id
76
+ )
77
+ )
78
+ end
79
+
80
+ def upsert(*attributes)
81
+ attributes.flatten!
82
+
83
+ PUT(PATH, attributes)
84
+ end
85
+ end
86
+
87
+ class Metafields < Endpoint
88
+ PATH = "customers/%d/metafields"
89
+ RESULT_INSTANCE = Metafield
90
+
91
+ def get(customer_id, options = {})
92
+ GET(path(customer_id), options)
93
+ end
94
+
95
+ def create(metafield)
96
+ metafield = metafield.to_h
97
+ id = metafield.delete(:resource_id)
98
+
99
+ if id.nil?
100
+ raise ArgumentError, "Cannot create customer metafield: given metafield record has no resource_id"
101
+ end
102
+
103
+ POST(path(id), metafield)
104
+ end
105
+
106
+ def update(metafield)
107
+ metafield = metafield.to_h
108
+
109
+ resource = metafield.delete(:resource_id)
110
+ if resource.nil?
111
+ raise ArgumentError, "Cannot update customer metafield: given metafield has no resource_id"
112
+ end
113
+
114
+ id = metafield.delete(:id)
115
+ if id.nil?
116
+ raise ArgumentError, "Cannot update customer metafield: given metafield has no id"
117
+ end
118
+
119
+ result = PUT(path(resource_id, id), metafield)
120
+ unwrap(result)
121
+ end
122
+
123
+ private
124
+
125
+ def path(customer_id, *rest)
126
+ path = sprintf(PATH, customer_id)
127
+ return path if rest.empty?
128
+
129
+ path << "/" << rest.join("/")
130
+ end
131
+ end
132
+
133
+ PATH = "customers"
134
+ RESULT_INSTANCE = Customer
135
+
136
+ attr_reader :addresses,
137
+ :attributes,
138
+ :attribute_values,
139
+ :metafields
140
+
141
+ def initialize(*argz)
142
+ super(*argz)
143
+
144
+ @addresses = Addresses.new(*argz)
145
+ @attributes = Attributes.new(*argz)
146
+ @attribute_values = AttributeValues.new(*argz)
147
+ @metafields = Metafields.new(*argz)
148
+ end
149
+
150
+ def create(*customers)
151
+ customers.flatten!
152
+
153
+ POST(PATH, customers.map(&:to_h))
154
+ end
155
+
156
+ def delete(*ids)
157
+ ids.flatten!
158
+
159
+ DELETE(
160
+ PATH,
161
+ with_in_param({:id => ids}, :id)
162
+ )
163
+ end
164
+
165
+ def get(options = {})
166
+ GET(
167
+ PATH,
168
+ with_in_param(
169
+ options,
170
+ :company,
171
+ :customer_group_id,
172
+ :email,
173
+ :id,
174
+ :name,
175
+ :registration_ip_address
176
+ )
177
+ )
178
+ end
179
+
180
+ def update(*customers)
181
+ customers.flatten!
182
+
183
+ PUT(PATH, customers.map(&:to_h))
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "big_commerce/management_api/version"
6
+
7
+ module BigCommerce
8
+ module ManagementAPI
9
+ Error = Class.new(StandardError)
10
+
11
+ class ResponseHeaders
12
+ def initialize(headers)
13
+ @headers = headers || {}
14
+ end
15
+
16
+ def [](name)
17
+ @headers[name]
18
+ end
19
+
20
+ def request_id
21
+ value = @headers["x-request-id"]
22
+ value.is_a?(Array) ? value[0] : value
23
+ end
24
+
25
+ %w[x-rate-limit-requests-left
26
+ x-rate-limit-time-reset-ms
27
+ x-rate-limit-requests-quota
28
+ x-rate-limit-time-window-ms].each do |header|
29
+
30
+ method = header.delete_prefix("x-")
31
+ method.tr!("-", "_")
32
+
33
+ define_method(method) do
34
+ value = @headers[header]
35
+ # Net::HTTP returns as an array
36
+ (value.is_a?(Array) ? value[0] : value).to_i
37
+ end
38
+ end
39
+ end
40
+
41
+ class ResponseError < Error
42
+ attr_reader :headers
43
+
44
+ def initialize(data, headers)
45
+ @headers = ResponseHeaders.new(headers)
46
+ @data = data
47
+
48
+ super error_message(data)
49
+ end
50
+
51
+ private
52
+
53
+ def error_message(data)
54
+ #
55
+ # Looks for a structure like one of these and picks best message:
56
+ #
57
+ # A:
58
+ #
59
+ # {"errors"=>
60
+ # [{"status"=>409,
61
+ # "title"=>"Cannot have multiple segments with the same name",
62
+ # "type"=>
63
+ # "https://developer.bigcommerce.com/api-docs/getting-started/api-status-codes",
64
+ # "errors"=>{}}]
65
+ #
66
+ #
67
+ # B:
68
+ #
69
+ # {"status"=>422,
70
+ # "title"=>"Set customer attribute values failed.",
71
+ # "type"=>
72
+ # "https://developer.bigcommerce.com/api-docs/getting-started/api-status-codes",
73
+ # "errors"=>{"0.data"=>"missing attribute value"}}
74
+ #
75
+ return data unless data.is_a?(Hash)
76
+
77
+ data = data["errors"][0] if data["errors"].is_a?(Array)
78
+
79
+ if data["errors"].any?
80
+ errors = data["errors"].map { |property, message| "#{property}: #{message.chomp(".")}" }
81
+ errors.join(", ")
82
+ else
83
+ title = data["title"].chomp(".")
84
+ sprintf("%s (%s)", title, data["status"])
85
+ end
86
+ end
87
+ end
88
+
89
+ class Endpoint
90
+ HOST = "api.bigcommerce.com"
91
+ PORT = 443
92
+
93
+ USER_AGENT = "BigCommerce Management API Client v#{BigCommerce::ManagementAPI::VERSION} (Ruby v#{RUBY_VERSION})"
94
+
95
+ CONTENT_TYPE = "Content-Type"
96
+ CONTENT_TYPE_JSON = "application/json"
97
+
98
+ JSON_CONTENT_TYPES = [CONTENT_TYPE_JSON, "application/problem+json"].freeze
99
+
100
+ RESULT_KEY = "data"
101
+
102
+ # May go in its own file
103
+ class Response
104
+ include Enumerable
105
+
106
+ # class Pagination
107
+ class2 self,
108
+ :pagination => {
109
+ :total => 0,
110
+ :count => 0,
111
+ :per_page => 0,
112
+ :current_page => 0,
113
+ :total_pages => 0,
114
+ :links => {
115
+ :previous => "string",
116
+ :current => "string",
117
+ :next => "string"
118
+ }
119
+ }
120
+
121
+ class Meta
122
+ attr_reader :pagination, :total, :success, :failed
123
+
124
+ def initialize(meta)
125
+ @total = meta["total"]
126
+ @success = meta["success"]
127
+ @failed = meta["failed"]
128
+
129
+ @pagination = Pagination.new(meta["pagination"]) if meta["pagination"]
130
+ end
131
+ end
132
+
133
+ attr_reader :meta, :headers
134
+
135
+ def initialize(headers, result = nil, meta = nil)
136
+ @result = result || []
137
+ @headers = ResponseHeaders.new(headers)
138
+ @meta = Meta.new(meta) if meta
139
+ end
140
+
141
+ def each(&block)
142
+ @result.each(&block)
143
+ end
144
+ end
145
+
146
+ def initialize(store_hash, auth_token, options = nil)
147
+ raise ArgumentError, "store hash required" if store_hash.to_s.empty?
148
+ raise ArgumentError, "auth token required" if auth_token.to_s.empty?
149
+
150
+ @store_hash = store_hash
151
+ @auth_token = auth_token
152
+ @options = options || {}
153
+ end
154
+
155
+ protected
156
+
157
+ def DELETE(path, data = {})
158
+ path = endpoint(path)
159
+ path << query_string(data) if data && data.any?
160
+
161
+ request(Net::HTTP::Delete.new(path), data)
162
+ end
163
+
164
+ def GET(path, data = {})
165
+ path = endpoint(path)
166
+ path << query_string(data) if data && data.any?
167
+
168
+ request(Net::HTTP::Get.new(path), data)
169
+ end
170
+
171
+ def POST(path, data = {})
172
+ request(Net::HTTP::Post.new(endpoint(path)), data)
173
+ end
174
+
175
+ def PUT(path, data = {})
176
+ request(Net::HTTP::Put.new(endpoint(path)), data)
177
+ end
178
+
179
+ # For better or worse all Response instances contain an Array
180
+ # In some cases we don't want this since response only contains a single result
181
+ def unwrap(result)
182
+ record = result.first
183
+ return unless record
184
+
185
+ record.meta = result.meta
186
+ record.headers = result.headers
187
+ record
188
+ end
189
+
190
+ def with_in_param(options, *param_names)
191
+ return unless options
192
+
193
+ options = options.dup
194
+ options.keys.each do |name|
195
+ # Remove optional ":in" portion from "id:in" which may or may not be a Symbol
196
+ name = name.to_s.split(":")[0].to_sym
197
+ next unless param_names.include?(name)
198
+
199
+ values = Array(options.delete(name))
200
+
201
+ in_name = "#{name}:in"
202
+ values.concat(Array(options.delete(in_name)))
203
+
204
+ next unless values.any?
205
+
206
+ options[in_name] = values
207
+ end
208
+
209
+ options
210
+ end
211
+
212
+ private
213
+
214
+ def query_string(params)
215
+ query = []
216
+
217
+ # We do this manually to join arrays and because time cannot be escaped as it will not be properly applied server-side
218
+ params.each do |name, value|
219
+ name = URI.encode_www_form_component(name)
220
+
221
+ if value.is_a?(Array)
222
+ value = value.join(",")
223
+ elsif value.respond_to?(:strftime)
224
+ value = value.strftime("%Y-%m-%dT%H:%M:%S%z")
225
+ else
226
+ value = URI.encode_www_form_component(value)
227
+ end
228
+
229
+ query << "#{name}=#{value}"
230
+ end
231
+
232
+ sprintf("?%s", query.join("&"))
233
+ end
234
+
235
+ def endpoint(path)
236
+ sprintf("/stores/%s/v3/%s", @store_hash, path)
237
+ end
238
+
239
+ # TODO: move this to request() so we can create ResponseError
240
+ def parse_json(s)
241
+ JSON.parse(s)
242
+ rescue JSON::ParserError => e
243
+ raise Error, "failed to parse response JSON: #{e}"
244
+ end
245
+
246
+ def request(req, data = {})
247
+ req["X-Auth-Token"] = @auth_token
248
+ req["User-Agent"] = USER_AGENT
249
+
250
+ if req.request_body_permitted? && data && data.any?
251
+ req.body = data.to_json
252
+ req[CONTENT_TYPE] = CONTENT_TYPE_JSON
253
+ end
254
+
255
+ request = Net::HTTP.new(HOST, PORT)
256
+ request.use_ssl = true
257
+
258
+ if !@options[:debug]
259
+ request.set_debug_output(nil)
260
+ else
261
+ request.set_debug_output(
262
+ @options[:debug].is_a?(IO) ? @options[:debug] : $stderr
263
+ )
264
+ end
265
+
266
+ request.start { |http| handle_response(http.request(req)) }
267
+ end
268
+
269
+ def handle_response(res)
270
+ # TODO: data can be HTML string! Don't want this in the error! Do we?
271
+ data = res.body && JSON_CONTENT_TYPES.include?(res[CONTENT_TYPE]) ? parse_json(res.body) : res.body
272
+ # pp data
273
+ # Otherwise only available by name via #[]
274
+ headers = res.to_hash
275
+
276
+ raise ResponseError.new(data, headers) if res.code[0] != "2"
277
+
278
+ # 204, likely
279
+ return Response.new(headers) unless data
280
+
281
+ result = data[self.class::RESULT_KEY]
282
+ if result.is_a?(Array)
283
+ result.map! { |data| self.class::RESULT_INSTANCE.new(data) }
284
+ else
285
+ result = [self.class::RESULT_INSTANCE.new(result)]
286
+ end
287
+
288
+ # If response code is 2XX and data is a String we will have an error here
289
+ # but it's TBD if this is ever the case
290
+ Response.new(headers, result, data["meta"])
291
+ end
292
+ end
293
+ end
294
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "big_commerce/management_api/endpoint"
4
+
5
+ module BigCommerce
6
+ module ManagementAPI
7
+ class Inventories
8
+ class Items < Endpoint
9
+ PATH = "inventory/items"
10
+ RESULT_INSTANCE = Inventory
11
+
12
+ def get(options = {})
13
+ GET(
14
+ PATH,
15
+ with_in_param(
16
+ options,
17
+ :location_code,
18
+ :location_id,
19
+ :product_id,
20
+ :sku,
21
+ :variant_id
22
+ )
23
+ )
24
+ end
25
+ end
26
+
27
+ attr_reader :items
28
+
29
+ def initialize(*argz)
30
+ @items = Items.new(*argz)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "big_commerce/management_api/endpoint"
4
+
5
+ module BigCommerce
6
+ module ManagementAPI
7
+ class Segments < Endpoint
8
+ PATH = "segments"
9
+ RESULT_INSTANCE = Segment
10
+
11
+ def create(*segments)
12
+ segments.flatten!
13
+
14
+ POST(PATH, segments.map(&:to_h))
15
+ end
16
+
17
+ def delete(*ids)
18
+ ids.flatten!
19
+
20
+ DELETE(
21
+ PATH,
22
+ with_in_param({:id => ids}, :id)
23
+ )
24
+ end
25
+
26
+ def get(options = {})
27
+ GET(
28
+ PATH,
29
+ with_in_param(
30
+ options,
31
+ :id
32
+ )
33
+ )
34
+ end
35
+
36
+ def update(*segments)
37
+ segments.flatten!
38
+
39
+ PUT(PATH, segments.map(&:to_h))
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "big_commerce/management_api/endpoint"
4
+
5
+ module BigCommerce
6
+ module ManagementAPI
7
+ class Subscribers < Endpoint
8
+ PATH = "customers/subscribers"
9
+ RESULT_INSTANCE = Subscriber
10
+
11
+ def create(attributes)
12
+ result = POST(PATH, attributes)
13
+ unwrap(result)
14
+ end
15
+
16
+ def delete(options)
17
+ DELETE(
18
+ PATH,
19
+ with_in_param(
20
+ options,
21
+ :date_created,
22
+ :date_modified,
23
+ :email,
24
+ :first_name,
25
+ :id,
26
+ :last_name,
27
+ :order_id,
28
+ :source
29
+ )
30
+ )
31
+ end
32
+
33
+ ##
34
+ #
35
+ # Given an ID find a single Subscriber. Given a Hash find Subscribers by the provided criteria.
36
+ #
37
+ def get(options_or_id)
38
+ query = options_or_id
39
+
40
+ if query.is_a?(Hash)
41
+ query = with_in_param(
42
+ query,
43
+ :date_created,
44
+ :date_modified,
45
+ :email,
46
+ :first_name,
47
+ :id,
48
+ :last_name,
49
+ :order_id,
50
+ :source
51
+ )
52
+ end
53
+
54
+ GET(PATH, query)
55
+ end
56
+
57
+ def update(attributes)
58
+ attributes = attributes.to_h
59
+
60
+ id = attributes.delete(:id)
61
+ if id.nil?
62
+ raise ArgumentError, "Cannot update subscriber: given subscriber has no id"
63
+ end
64
+
65
+ result = UPDATE("#{PATH}/#{id}", attributes)
66
+ unwrap(result)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BigCommerce
4
+ module ManagementAPI
5
+ VERSION = "0.0.1"
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "big_commerce/management_api/classes"
4
+
5
+ module BigCommerce
6
+ module ManagementAPI
7
+ def self.new(*argz)
8
+ Client.new(*argz)
9
+ end
10
+
11
+ class Client
12
+ attr_reader :customers,
13
+ :inventories,
14
+ :segments,
15
+ :subscribers
16
+
17
+ def initialize(*argz)
18
+ @customers = ManagementAPI::Customers.new(*argz)
19
+ @inventories = ManagementAPI::Inventories.new(*argz)
20
+ @segments = ManagementAPI::Segments.new(*argz)
21
+ @subscribers = ManagementAPI::Subscribers.new(*argz)
22
+ end
23
+ end
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: big_commerce-management_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Skye Shaw
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-11-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: class2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.6.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.6.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: vcr
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: webmock
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dotenv
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: v3 API client for BigCommerce's REST Management API. Implementation is
70
+ far from complete but adding support for new endpoints is trivial.
71
+ email:
72
+ - skye.shaw@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".env.test.example"
78
+ - ".github/workflows/ci.yml"
79
+ - ".gitignore"
80
+ - ".rspec"
81
+ - Gemfile
82
+ - LICENSE.txt
83
+ - README.md
84
+ - Rakefile
85
+ - big_commerce-management_api.gemspec
86
+ - bin/console
87
+ - bin/setup
88
+ - etc/customers.csv
89
+ - lib/big_commerce/management_api.rb
90
+ - lib/big_commerce/management_api/classes.json
91
+ - lib/big_commerce/management_api/classes.rb
92
+ - lib/big_commerce/management_api/customers.rb
93
+ - lib/big_commerce/management_api/endpoint.rb
94
+ - lib/big_commerce/management_api/inventories.rb
95
+ - lib/big_commerce/management_api/segments.rb
96
+ - lib/big_commerce/management_api/subscribers.rb
97
+ - lib/big_commerce/management_api/version.rb
98
+ homepage: https://github.com/ScreenStaring/big_commerce-management_api
99
+ licenses:
100
+ - MIT
101
+ metadata:
102
+ homepage_uri: https://github.com/ScreenStaring/big_commerce-management_api
103
+ source_code_uri: https://github.com/ScreenStaring/big_commerce-management_api
104
+ bug_tracker_uri: https://github.com/ScreenStaring/big_commerce-management_api/issues
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: 2.3.0
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubygems_version: 3.1.6
121
+ signing_key:
122
+ specification_version: 4
123
+ summary: v3 API client for BigCommerce's REST Management API
124
+ test_files: []