big_commerce-management_api 0.0.1

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 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: []