clickfunnels-ruby-sdk 0.1.0
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 +7 -0
- data/CHANGELOG.md +31 -0
- data/LICENSE +21 -0
- data/README.md +190 -0
- data/lib/cf/auth.rb +16 -0
- data/lib/cf/client.rb +162 -0
- data/lib/cf/configuration.rb +44 -0
- data/lib/cf/errors.rb +31 -0
- data/lib/cf/http/client.rb +35 -0
- data/lib/cf/http/net_http_adapter.rb +78 -0
- data/lib/cf/logger.rb +31 -0
- data/lib/cf/resources/base.rb +530 -0
- data/lib/cf/version.rb +5 -0
- data/lib/cf.rb +32 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5bf38ce3e85e117aee729468402ed494872b514f2c140f2af82c5726631e8c2e
|
4
|
+
data.tar.gz: cfed4a99267fc46fd30bafa227854a7b92ba9dc3d0f943be4dfa6cb1925bb144
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 22170fb6c932e6498371dcc7e2cc9881a9119418d6aadcbfa1b3edee5db89f20acc0c05e19d367939686feaf0777cf2a7be174ae477123d368462b423f975146
|
7
|
+
data.tar.gz: 790f7605f1ea571582ef9e9ff5fd2311dab374c954dc5784d73186239988128a60035b0827f83e60d0f5902180313509cd868ffcca7c09ccf9d6b10ba4d7156f
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [0.1.0] - 2025-06-21
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Initial implementation of ClickFunnels Ruby SDK
|
12
|
+
- Complete Ruby SDK implementation based on OpenAPI spec
|
13
|
+
- HTTP client with configurable adapters (Net::HTTP default)
|
14
|
+
- Resource-based API with method mapping for all ClickFunnels endpoints
|
15
|
+
- Comprehensive test suite with WebMock integration
|
16
|
+
- Debug logging with configurable file logger
|
17
|
+
- Error handling with structured error responses
|
18
|
+
- Bearer token authentication
|
19
|
+
- Support for workspace-scoped and team-scoped resources
|
20
|
+
- Dynamic resource path building with parent ID support
|
21
|
+
- MIT License
|
22
|
+
|
23
|
+
### Features
|
24
|
+
- 138+ auto-generated resource classes covering all ClickFunnels API endpoints
|
25
|
+
- Configurable subdomain, API token, workspace ID, and team ID
|
26
|
+
- Graceful error handling returning structured error objects instead of exceptions
|
27
|
+
- Query parameter support for filtering, pagination, and search
|
28
|
+
- Full CRUD operations (GET, POST, PUT, PATCH, DELETE) for all resources
|
29
|
+
- Comprehensive documentation and interactive testing environment
|
30
|
+
|
31
|
+
[0.1.0]: https://github.com/RichStone/clickfunnels-ruby-sdk/releases/tag/v0.1.0
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Rich Steinmetz
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
# 🚧 ClickFunnels Ruby SDK (unofficial) - WIP 🚧
|
2
|
+
|
3
|
+
A simple, dynamic Ruby SDK for the ClickFunnels API with support for all endpoints and operations mostly generated by Claude Code.
|
4
|
+
|
5
|
+
🚧 Before using in prod, this client will need additional fixing and testing. Let the authors know if you would like to use it to accelerate. 🚧
|
6
|
+
|
7
|
+
## Quick Start Testing
|
8
|
+
|
9
|
+
```bash
|
10
|
+
# Or jump straight to IRB with pre-configured environment
|
11
|
+
bundle exec irb -r ./irb_test.rb
|
12
|
+
```
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem "clickfunnels-ruby-sdk"
|
20
|
+
```
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
$ bundle install
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
|
28
|
+
$ gem install clickfunnels-ruby-sdk
|
29
|
+
|
30
|
+
## Configuration
|
31
|
+
|
32
|
+
Configure the SDK with your ClickFunnels subdomain and API token:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
CF.configure do |config|
|
36
|
+
config.subdomain = "myaccount" # Your ClickFunnels subdomain
|
37
|
+
config.api_token = "your_bearer_token" # Your API token
|
38
|
+
config.workspace_id = "ws_123" # Default workspace ID (optional)
|
39
|
+
config.team_id = "team_456" # Default team ID (optional)
|
40
|
+
config.debug = true # Enable debug logging (optional)
|
41
|
+
config.timeout = 60 # Request timeout in seconds (optional)
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
## Usage
|
46
|
+
|
47
|
+
The SDK provides dynamic access to all ClickFunnels API endpoints following the same structure as documented in the API.
|
48
|
+
|
49
|
+
### Basic CRUD Operations
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
# List resources
|
53
|
+
CF::Orders::Invoice.list
|
54
|
+
|
55
|
+
# Get a specific resource
|
56
|
+
CF::Orders::Invoice.get(123)
|
57
|
+
|
58
|
+
# Create a new resource
|
59
|
+
CF::Orders::Invoice.create(amount: 100, currency: "USD")
|
60
|
+
|
61
|
+
# Update a resource
|
62
|
+
CF::Orders::Invoice.update(123, status: "paid")
|
63
|
+
|
64
|
+
# Delete a resource
|
65
|
+
CF::Orders::Invoice.delete(123)
|
66
|
+
```
|
67
|
+
|
68
|
+
### 🆕 Workspace-Nested Resources
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
# Using default workspace_id from configuration
|
72
|
+
CF::Workspaces::Contact.list
|
73
|
+
CF::Workspaces::Contact.get(123)
|
74
|
+
CF::Workspaces::Contact.create(email: "test@example.com", name: "John Doe")
|
75
|
+
|
76
|
+
# Override workspace_id for specific requests
|
77
|
+
CF::Workspaces::Contact.list(workspace_id: "different_workspace")
|
78
|
+
CF::Workspaces::Contact.create(
|
79
|
+
{ email: "test@example.com" },
|
80
|
+
{ workspace_id: "custom_workspace" }
|
81
|
+
)
|
82
|
+
```
|
83
|
+
|
84
|
+
### Pagination
|
85
|
+
|
86
|
+
Use cursor-based pagination with `after` and `before` parameters:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
# Get invoices after a specific ID
|
90
|
+
CF::Orders::Invoice.list(after: { id: 123 })
|
91
|
+
|
92
|
+
# Get invoices before a specific ID
|
93
|
+
CF::Orders::Invoice.list(before: { id: 456 })
|
94
|
+
```
|
95
|
+
|
96
|
+
### Filtering
|
97
|
+
|
98
|
+
Apply filters to list operations:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
# Filter by status
|
102
|
+
CF::Orders::Invoice.list(filter: { status: "paid" })
|
103
|
+
|
104
|
+
# Filter by multiple values
|
105
|
+
CF::Orders::Invoice.list(filter: { id: [1, 2, 3] })
|
106
|
+
|
107
|
+
# Multiple filters
|
108
|
+
CF::Orders::Invoice.list(filter: { status: "paid", currency: "USD" })
|
109
|
+
```
|
110
|
+
|
111
|
+
### Sorting
|
112
|
+
|
113
|
+
Sort results using `sort_property` and `sort_order`:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
CF::Orders::Invoice.list(
|
117
|
+
sort_property: "created_at",
|
118
|
+
sort_order: "desc"
|
119
|
+
)
|
120
|
+
```
|
121
|
+
|
122
|
+
## Debug Logging
|
123
|
+
|
124
|
+
Enable debug logging to see all HTTP requests and responses:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
CF.configure do |config|
|
128
|
+
config.debug = true
|
129
|
+
# Logs will be written to cf_sdk.log by default
|
130
|
+
end
|
131
|
+
|
132
|
+
# Or provide your own logger
|
133
|
+
CF.configure do |config|
|
134
|
+
config.debug = true
|
135
|
+
config.logger = Logger.new(STDOUT)
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
## Error Handling
|
140
|
+
|
141
|
+
The SDK returns error objects instead of raising exceptions.
|
142
|
+
|
143
|
+
## Testing
|
144
|
+
|
145
|
+
### Run Test Suite
|
146
|
+
```bash
|
147
|
+
bundle exec rake test
|
148
|
+
```
|
149
|
+
|
150
|
+
### Interactive Testing
|
151
|
+
|
152
|
+
**Default IRB Test Environment (Recommended):**
|
153
|
+
|
154
|
+
```bash
|
155
|
+
bundle exec irb -r ./irb_test.rb
|
156
|
+
```
|
157
|
+
|
158
|
+
This loads a pre-configured test environment with:
|
159
|
+
- ✅ All new features enabled
|
160
|
+
- ✅ Sample configuration with default workspace/team IDs
|
161
|
+
- ✅ Resource examples and usage tips
|
162
|
+
- ✅ Path generation demonstrations
|
163
|
+
|
164
|
+
**Manual IRB Setup:**
|
165
|
+
```bash
|
166
|
+
bundle exec irb -r ./lib/cf
|
167
|
+
```
|
168
|
+
|
169
|
+
Then configure manually:
|
170
|
+
```ruby
|
171
|
+
CF.configure do |config|
|
172
|
+
config.subdomain = "your_subdomain"
|
173
|
+
config.api_token = "your_token"
|
174
|
+
config.workspace_id = "your_workspace_id" # Optional default
|
175
|
+
config.debug = true
|
176
|
+
end
|
177
|
+
|
178
|
+
# Test the new features
|
179
|
+
CF::Workspaces::Contact.list
|
180
|
+
CF::Orders::Invoice.list
|
181
|
+
CF::Users.list
|
182
|
+
```
|
183
|
+
|
184
|
+
## Contributing
|
185
|
+
|
186
|
+
Bug reports and pull requests are welcome on GitHub.
|
187
|
+
|
188
|
+
## License
|
189
|
+
|
190
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/lib/cf/auth.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CF
|
4
|
+
module Auth
|
5
|
+
def self.bearer_token_headers(token)
|
6
|
+
{
|
7
|
+
"Authorization" => "Bearer #{token}",
|
8
|
+
"User-Agent" => "CF Ruby SDK #{CF::VERSION}"
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.validate_token!(token)
|
13
|
+
raise CF::AuthenticationError, "API token is required" if token.nil? || token.empty?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/cf/client.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "cgi"
|
5
|
+
|
6
|
+
module CF
|
7
|
+
class Client
|
8
|
+
def initialize(configuration)
|
9
|
+
@configuration = configuration
|
10
|
+
@http_client = CF::HTTP::Client.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(path, params = {})
|
14
|
+
request(:get, path, nil, params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def post(path, body = nil, params = {})
|
18
|
+
request(:post, path, body, params)
|
19
|
+
end
|
20
|
+
|
21
|
+
def put(path, body = nil, params = {})
|
22
|
+
request(:put, path, body, params)
|
23
|
+
end
|
24
|
+
|
25
|
+
def patch(path, body = nil, params = {})
|
26
|
+
request(:patch, path, body, params)
|
27
|
+
end
|
28
|
+
|
29
|
+
def delete(path, params = {})
|
30
|
+
request(:delete, path, nil, params)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :configuration, :http_client
|
36
|
+
|
37
|
+
def request(method, path, body = nil, params = {})
|
38
|
+
validate_configuration!
|
39
|
+
|
40
|
+
url = build_url(path, params)
|
41
|
+
headers = build_headers
|
42
|
+
|
43
|
+
log_request(method, url, body, headers) if configuration.log_requests?
|
44
|
+
|
45
|
+
case method
|
46
|
+
when :get, :delete
|
47
|
+
response = http_client.send(method, url, headers)
|
48
|
+
else
|
49
|
+
response = http_client.send(method, url, body, headers)
|
50
|
+
end
|
51
|
+
|
52
|
+
log_response(response) if configuration.log_requests?
|
53
|
+
|
54
|
+
handle_response(response)
|
55
|
+
rescue => e
|
56
|
+
log_error(e) if configuration.log_requests?
|
57
|
+
raise
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate_configuration!
|
61
|
+
CF::Auth.validate_token!(configuration.api_token)
|
62
|
+
raise CF::ConfigurationError, "Configuration is invalid" unless configuration.valid?
|
63
|
+
end
|
64
|
+
|
65
|
+
def build_url(path, params = {})
|
66
|
+
url = "#{configuration.base_url}#{path}"
|
67
|
+
return url if params.empty?
|
68
|
+
|
69
|
+
query_string = build_query_string(params)
|
70
|
+
"#{url}?#{query_string}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def build_headers
|
74
|
+
headers = CF::Auth.bearer_token_headers(configuration.api_token)
|
75
|
+
headers["Accept"] = "application/json"
|
76
|
+
headers["Content-Type"] = "application/json"
|
77
|
+
headers
|
78
|
+
end
|
79
|
+
|
80
|
+
def build_query_string(params)
|
81
|
+
params.map do |key, value|
|
82
|
+
if value.is_a?(Array)
|
83
|
+
value.map { |v| "#{key}[]=#{CGI.escape(v.to_s)}" }.join("&")
|
84
|
+
elsif value.is_a?(Hash)
|
85
|
+
value.map { |k, v| "#{key}[#{k}]=#{CGI.escape(v.to_s)}" }.join("&")
|
86
|
+
else
|
87
|
+
"#{key}=#{CGI.escape(value.to_s)}"
|
88
|
+
end
|
89
|
+
end.join("&")
|
90
|
+
end
|
91
|
+
|
92
|
+
def handle_response(response)
|
93
|
+
case response[:status]
|
94
|
+
when 200..299
|
95
|
+
parse_response_body(response[:body])
|
96
|
+
when 400
|
97
|
+
create_error_response("Bad Request", CF::BadRequestError, response)
|
98
|
+
when 401
|
99
|
+
create_error_response("Unauthorized", CF::UnauthorizedError, response)
|
100
|
+
when 403
|
101
|
+
create_error_response("Forbidden", CF::ForbiddenError, response)
|
102
|
+
when 404
|
103
|
+
create_error_response("Not Found", CF::NotFoundError, response)
|
104
|
+
when 422
|
105
|
+
create_error_response("Unprocessable Entity", CF::UnprocessableEntityError, response)
|
106
|
+
when 429
|
107
|
+
create_error_response("Too Many Requests", CF::TooManyRequestsError, response)
|
108
|
+
when 500
|
109
|
+
create_error_response("Internal Server Error", CF::InternalServerError, response)
|
110
|
+
when 502
|
111
|
+
create_error_response("Bad Gateway", CF::BadGatewayError, response)
|
112
|
+
when 503
|
113
|
+
create_error_response("Service Unavailable", CF::ServiceUnavailableError, response)
|
114
|
+
when 504
|
115
|
+
create_error_response("Gateway Timeout", CF::GatewayTimeoutError, response)
|
116
|
+
else
|
117
|
+
create_error_response("HTTP #{response[:status]}", CF::APIError, response)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def parse_response_body(body)
|
122
|
+
return nil if body.nil? || body.empty?
|
123
|
+
|
124
|
+
JSON.parse(body, symbolize_names: true)
|
125
|
+
rescue JSON::ParserError
|
126
|
+
body
|
127
|
+
end
|
128
|
+
|
129
|
+
def create_error_response(message, error_class, response)
|
130
|
+
{
|
131
|
+
error: true,
|
132
|
+
error_type: error_class.name,
|
133
|
+
message: message,
|
134
|
+
status: response[:status],
|
135
|
+
response_body: response[:body],
|
136
|
+
# response_headers: response[:headers]
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
def log_request(method, url, body, headers)
|
141
|
+
return unless configuration.logger
|
142
|
+
|
143
|
+
configuration.logger.info("[CF SDK] Request: #{method.upcase} #{url}")
|
144
|
+
# configuration.logger.debug("[CF SDK] Headers: #{headers.inspect}")
|
145
|
+
configuration.logger.debug("[CF SDK] Body: #{body}") if body
|
146
|
+
end
|
147
|
+
|
148
|
+
def log_response(response)
|
149
|
+
return unless configuration.logger
|
150
|
+
|
151
|
+
configuration.logger.info("[CF SDK] Response: #{response[:status]}")
|
152
|
+
# configuration.logger.debug("[CF SDK] Response Headers: #{response[:headers]}")
|
153
|
+
configuration.logger.debug("[CF SDK] Response Body: #{response[:body]}")
|
154
|
+
end
|
155
|
+
|
156
|
+
def log_error(error)
|
157
|
+
return unless configuration.logger
|
158
|
+
|
159
|
+
configuration.logger.error("[CF SDK] Error: #{error.class.name} - #{error.message}")
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CF
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :subdomain, :api_token, :api_version, :debug, :timeout, :workspace_id, :team_id
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@subdomain = nil
|
9
|
+
@api_token = nil
|
10
|
+
@api_version = "v2"
|
11
|
+
@debug = false
|
12
|
+
@timeout = 30
|
13
|
+
@logger = nil
|
14
|
+
@workspace_id = nil
|
15
|
+
@team_id = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def base_url
|
19
|
+
raise CF::ConfigurationError, "Subdomain is required" unless subdomain
|
20
|
+
|
21
|
+
"https://#{subdomain}.myclickfunnels.com/api/#{api_version}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid?
|
25
|
+
!subdomain.nil? && !api_token.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
def debug?
|
29
|
+
debug
|
30
|
+
end
|
31
|
+
|
32
|
+
def log_requests?
|
33
|
+
debug?
|
34
|
+
end
|
35
|
+
|
36
|
+
def logger
|
37
|
+
@logger ||= debug? ? CF::FileLogger.new : nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def logger=(custom_logger)
|
41
|
+
@logger = custom_logger
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/cf/errors.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CF
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class ConfigurationError < Error; end
|
7
|
+
|
8
|
+
class AuthenticationError < Error; end
|
9
|
+
|
10
|
+
class APIError < Error
|
11
|
+
attr_reader :status, :response_body, :response_headers
|
12
|
+
|
13
|
+
def initialize(message, status: nil, response_body: nil, response_headers: nil)
|
14
|
+
super(message)
|
15
|
+
@status = status
|
16
|
+
@response_body = response_body
|
17
|
+
@response_headers = response_headers
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class BadRequestError < APIError; end
|
22
|
+
class UnauthorizedError < APIError; end
|
23
|
+
class ForbiddenError < APIError; end
|
24
|
+
class NotFoundError < APIError; end
|
25
|
+
class UnprocessableEntityError < APIError; end
|
26
|
+
class TooManyRequestsError < APIError; end
|
27
|
+
class InternalServerError < APIError; end
|
28
|
+
class BadGatewayError < APIError; end
|
29
|
+
class ServiceUnavailableError < APIError; end
|
30
|
+
class GatewayTimeoutError < APIError; end
|
31
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CF
|
4
|
+
module HTTP
|
5
|
+
class Client
|
6
|
+
def initialize(adapter = nil)
|
7
|
+
@adapter = adapter || NetHTTPAdapter.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def get(url, headers = {})
|
11
|
+
@adapter.request(:get, url, nil, headers)
|
12
|
+
end
|
13
|
+
|
14
|
+
def post(url, body = nil, headers = {})
|
15
|
+
@adapter.request(:post, url, body, headers)
|
16
|
+
end
|
17
|
+
|
18
|
+
def put(url, body = nil, headers = {})
|
19
|
+
@adapter.request(:put, url, body, headers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def patch(url, body = nil, headers = {})
|
23
|
+
@adapter.request(:patch, url, body, headers)
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(url, headers = {})
|
27
|
+
@adapter.request(:delete, url, nil, headers)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :adapter
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "uri"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
module CF
|
8
|
+
module HTTP
|
9
|
+
class NetHTTPAdapter
|
10
|
+
def initialize(options = {})
|
11
|
+
@options = {
|
12
|
+
timeout: 30,
|
13
|
+
open_timeout: 10,
|
14
|
+
use_ssl: true,
|
15
|
+
verify_mode: OpenSSL::SSL::VERIFY_PEER
|
16
|
+
}.merge(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def request(method, url, body = nil, headers = {})
|
20
|
+
uri = URI(url)
|
21
|
+
|
22
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
23
|
+
configure_http(http)
|
24
|
+
|
25
|
+
request = build_request(method, uri, body, headers)
|
26
|
+
response = http.request(request)
|
27
|
+
|
28
|
+
handle_response(response)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def configure_http(http)
|
35
|
+
http.read_timeout = @options[:timeout]
|
36
|
+
http.open_timeout = @options[:open_timeout]
|
37
|
+
|
38
|
+
if @options[:use_ssl]
|
39
|
+
http.use_ssl = true
|
40
|
+
http.verify_mode = @options[:verify_mode]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_request(method, uri, body, headers)
|
45
|
+
request_class = case method
|
46
|
+
when :get then Net::HTTP::Get
|
47
|
+
when :post then Net::HTTP::Post
|
48
|
+
when :put then Net::HTTP::Put # FIXME: Remove PUT.
|
49
|
+
when :patch then Net::HTTP::Patch
|
50
|
+
when :delete then Net::HTTP::Delete
|
51
|
+
else
|
52
|
+
raise ArgumentError, "Unsupported HTTP method: #{method}"
|
53
|
+
end
|
54
|
+
|
55
|
+
request = request_class.new(uri)
|
56
|
+
|
57
|
+
# Set headers
|
58
|
+
headers.each { |key, value| request[key] = value }
|
59
|
+
|
60
|
+
# Set body for POST/PUT/PATCH requests
|
61
|
+
if body && [:post, :put, :patch].include?(method)
|
62
|
+
request.body = body.is_a?(String) ? body : JSON.generate(body)
|
63
|
+
request["Content-Type"] = "application/json" unless request["Content-Type"]
|
64
|
+
end
|
65
|
+
|
66
|
+
request
|
67
|
+
end
|
68
|
+
|
69
|
+
def handle_response(response)
|
70
|
+
{
|
71
|
+
status: response.code.to_i,
|
72
|
+
headers: response.to_hash,
|
73
|
+
body: response.body
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/cf/logger.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module CF
|
6
|
+
class FileLogger
|
7
|
+
def initialize(log_file = "cf_sdk.log")
|
8
|
+
@logger = Logger.new(log_file)
|
9
|
+
@logger.level = Logger::DEBUG
|
10
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
11
|
+
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def info(message)
|
16
|
+
@logger.info(message)
|
17
|
+
end
|
18
|
+
|
19
|
+
def debug(message)
|
20
|
+
@logger.debug(message)
|
21
|
+
end
|
22
|
+
|
23
|
+
def error(message)
|
24
|
+
@logger.error(message)
|
25
|
+
end
|
26
|
+
|
27
|
+
def warn(message)
|
28
|
+
@logger.warn(message)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,530 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CF
|
4
|
+
module Resources
|
5
|
+
class Base
|
6
|
+
class << self
|
7
|
+
def client
|
8
|
+
CF.client
|
9
|
+
end
|
10
|
+
|
11
|
+
def list(params = {})
|
12
|
+
path = build_path("list", nil, params)
|
13
|
+
query_params = params.reject { |k, _v| parent_id_param?(k) }
|
14
|
+
client.get(path, query_params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(id, params = {})
|
18
|
+
path = build_path("get", id, params)
|
19
|
+
query_params = params.reject { |k, _v| parent_id_param?(k) }
|
20
|
+
client.get(path, query_params)
|
21
|
+
end
|
22
|
+
|
23
|
+
def create(attributes = {}, params = {})
|
24
|
+
# Extract parent IDs from attributes based on path convention
|
25
|
+
extracted_params = extract_parent_ids_from_attributes(attributes)
|
26
|
+
attributes = extracted_params[:attributes]
|
27
|
+
params = params.merge(extracted_params[:params])
|
28
|
+
|
29
|
+
path = build_path("create", nil, params)
|
30
|
+
query_params = params.reject { |k, _v| parent_id_param?(k) }
|
31
|
+
wrapped_attributes = wrap_attributes_for_request(attributes)
|
32
|
+
client.post(path, wrapped_attributes, query_params)
|
33
|
+
end
|
34
|
+
|
35
|
+
def update(id, attributes = {}, params = {})
|
36
|
+
# Extract parent IDs from attributes based on path convention
|
37
|
+
extracted_params = extract_parent_ids_from_attributes(attributes)
|
38
|
+
attributes = extracted_params[:attributes]
|
39
|
+
params = params.merge(extracted_params[:params])
|
40
|
+
|
41
|
+
path = build_path("update", id, params)
|
42
|
+
query_params = params.reject { |k, _v| parent_id_param?(k) }
|
43
|
+
wrapped_attributes = wrap_attributes_for_request(attributes)
|
44
|
+
client.patch(path, wrapped_attributes, query_params)
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete(id, params = {})
|
48
|
+
path = build_path("delete", id, params)
|
49
|
+
query_params = params.reject { |k, _v| parent_id_param?(k) }
|
50
|
+
client.delete(path, query_params)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def build_path(action, id = nil, params = {})
|
56
|
+
segments = resource_path_segments(params)
|
57
|
+
|
58
|
+
case action
|
59
|
+
when "list"
|
60
|
+
"/#{segments.join("/")}"
|
61
|
+
when "create"
|
62
|
+
build_create_path(segments, params)
|
63
|
+
when "get", "update", "delete"
|
64
|
+
"/#{segments.join("/")}/#{id}"
|
65
|
+
else
|
66
|
+
raise ArgumentError, "Unknown action: #{action}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def resource_path_segments(params = {})
|
71
|
+
# Convert class name to path segments
|
72
|
+
# CF::Orders::Invoice -> ["orders", "invoices"]
|
73
|
+
# CF::Workspaces::Contact -> ["workspaces", "123", "contacts"] (with workspace_id: 123)
|
74
|
+
# CF::Contacts::AppliedTag -> ["contacts", "123", "applied_tags"] (with contact_id: 123)
|
75
|
+
# CF::Contacts::Tag -> ["workspaces", "123", "contacts", "tags"] (special case)
|
76
|
+
|
77
|
+
parts = name.split("::")
|
78
|
+
parts.shift # Remove "CF"
|
79
|
+
|
80
|
+
# Check for special workspace-nested patterns
|
81
|
+
special_pattern = special_workspace_pattern(parts)
|
82
|
+
if special_pattern
|
83
|
+
return build_special_workspace_path(special_pattern, params)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Special case for Orders::Invoice - can be workspace OR order nested
|
87
|
+
if parts == ["Orders", "Invoice"] && !params.key?(:order_id)
|
88
|
+
return build_special_workspace_path({ path_suffix: "orders/invoices" }, params)
|
89
|
+
end
|
90
|
+
|
91
|
+
segments = []
|
92
|
+
parts.each_with_index do |part, index|
|
93
|
+
segment = part.gsub(/([A-Z])/, "_\\1").downcase.gsub(/^_/, "")
|
94
|
+
|
95
|
+
# For the final resource (last part), make it plural for the URL
|
96
|
+
if index == parts.length - 1
|
97
|
+
# Last segment - this is the resource, make it plural
|
98
|
+
segments << pluralize(segment)
|
99
|
+
else
|
100
|
+
# Namespace - use as-is (should already be plural in new convention)
|
101
|
+
segments << segment
|
102
|
+
|
103
|
+
# Check if this specific namespace needs a parent ID
|
104
|
+
parent_id_key = "#{segment.chomp("s")}_id".to_sym
|
105
|
+
|
106
|
+
# Always check if the parent ID is provided in params
|
107
|
+
if params.key?(parent_id_key)
|
108
|
+
# If parent ID is explicitly provided, use it (enables nested access)
|
109
|
+
segments << params[parent_id_key].to_s
|
110
|
+
elsif needs_parent_id_for_namespace?(segment, parts)
|
111
|
+
# If this namespace requires a parent ID by default, get it from config
|
112
|
+
parent_id = get_default_parent_id(parent_id_key)
|
113
|
+
|
114
|
+
if parent_id
|
115
|
+
segments << parent_id.to_s
|
116
|
+
else
|
117
|
+
raise CF::ConfigurationError, "#{parent_id_key} is required. Provide it as a parameter or set it in configuration."
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
segments
|
124
|
+
end
|
125
|
+
|
126
|
+
def needs_parent_id_for_namespace?(segment, parts)
|
127
|
+
# Determine which namespaces actually need parent IDs based on API structure
|
128
|
+
# This is more specific than just "all multi-part resources"
|
129
|
+
|
130
|
+
case segment.downcase
|
131
|
+
when "workspaces"
|
132
|
+
# CF::Workspaces::Contact -> needs workspace_id
|
133
|
+
true
|
134
|
+
when "teams"
|
135
|
+
# CF::Teams::Workspace -> needs team_id
|
136
|
+
true
|
137
|
+
when "contacts"
|
138
|
+
# CF::Contacts::AppliedTag -> needs contact_id
|
139
|
+
true
|
140
|
+
when "courses"
|
141
|
+
# CF::Courses::Section, CF::Courses::Enrollment -> needs course_id
|
142
|
+
true
|
143
|
+
when "orders"
|
144
|
+
# CF::Orders::Invoice might be top-level collection OR nested
|
145
|
+
# We'll check if order_id is provided in params to decide
|
146
|
+
false # Default to top-level for now
|
147
|
+
when "products"
|
148
|
+
# CF::Products::Price -> needs product_id if it's nested
|
149
|
+
false # Default to top-level for now
|
150
|
+
when "forms"
|
151
|
+
# CF::Forms::FieldSet -> might need form_id
|
152
|
+
false # Default to top-level for now
|
153
|
+
else
|
154
|
+
# For other cases, check if it's a known nested pattern
|
155
|
+
false
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def get_default_parent_id(parent_id_key)
|
160
|
+
case parent_id_key
|
161
|
+
when :workspace_id
|
162
|
+
CF.configuration.workspace_id
|
163
|
+
when :team_id
|
164
|
+
CF.configuration.team_id
|
165
|
+
when :contact_id, :course_id, :order_id, :product_id, :form_id
|
166
|
+
# These don't have defaults - must be provided
|
167
|
+
nil
|
168
|
+
else
|
169
|
+
nil
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def parent_id_param?(key)
|
174
|
+
# Any parameter ending with "_id" is considered a parent ID parameter
|
175
|
+
key.to_s.end_with?("_id")
|
176
|
+
end
|
177
|
+
|
178
|
+
def extract_parent_ids_from_attributes(attributes)
|
179
|
+
# Extract parent IDs that match the path convention from attributes
|
180
|
+
# For CF::Contacts::AppliedTag, look for contact_id in attributes
|
181
|
+
# For CF::Products::Price, look for product_id in attributes
|
182
|
+
|
183
|
+
expected_parent_ids = get_expected_parent_ids_for_class
|
184
|
+
params = {}
|
185
|
+
remaining_attributes = {}
|
186
|
+
|
187
|
+
attributes.each do |key, value|
|
188
|
+
if expected_parent_ids.include?(key.to_sym)
|
189
|
+
params[key] = value
|
190
|
+
else
|
191
|
+
remaining_attributes[key] = value
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
{ attributes: remaining_attributes, params: params }
|
196
|
+
end
|
197
|
+
|
198
|
+
def get_expected_parent_ids_for_class
|
199
|
+
# Based on the class name, determine what parent IDs are expected
|
200
|
+
# CF::Contacts::AppliedTag -> [:contact_id]
|
201
|
+
# CF::Products::Price -> [:product_id]
|
202
|
+
# CF::Workspaces::Contact -> [:workspace_id]
|
203
|
+
|
204
|
+
parts = name.split("::")[1..-1] # Remove "CF"
|
205
|
+
return [] if parts.length <= 1
|
206
|
+
|
207
|
+
parent_ids = []
|
208
|
+
|
209
|
+
# Check each namespace part for expected parent IDs
|
210
|
+
parts[0..-2].each do |part|
|
211
|
+
segment = part.gsub(/([A-Z])/, "_\\1").downcase.gsub(/^_/, "")
|
212
|
+
parent_id_key = "#{segment.chomp('s')}_id".to_sym
|
213
|
+
parent_ids << parent_id_key
|
214
|
+
end
|
215
|
+
|
216
|
+
parent_ids
|
217
|
+
end
|
218
|
+
|
219
|
+
def build_create_path(segments, params)
|
220
|
+
# For create operations, check if we need parent ID in the path
|
221
|
+
# This handles cases like CF::Products::Price.create(product_id: 123)
|
222
|
+
# which should POST to /products/123/prices instead of /products/prices
|
223
|
+
|
224
|
+
return "/#{segments.join("/")}" if segments.length <= 1
|
225
|
+
|
226
|
+
# Check if we have a parent resource that needs ID in the path
|
227
|
+
parent_resource = segments[-2] # Second to last segment
|
228
|
+
resource = segments[-1] # Last segment
|
229
|
+
|
230
|
+
# Build parent ID parameter name from parent resource
|
231
|
+
parent_id_key = "#{parent_resource.chomp('s')}_id".to_sym
|
232
|
+
|
233
|
+
if params.key?(parent_id_key)
|
234
|
+
# Replace the parent collection with parent/id pattern
|
235
|
+
path_segments = segments.dup
|
236
|
+
path_segments[-2] = "#{parent_resource}/#{params[parent_id_key]}"
|
237
|
+
path_segments.pop # Remove the resource name
|
238
|
+
path_segments << resource # Add it back
|
239
|
+
"/#{path_segments.join("/")}"
|
240
|
+
else
|
241
|
+
# No parent ID provided, use collection path
|
242
|
+
"/#{segments.join("/")}"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def wrap_attributes_for_request(attributes)
|
247
|
+
# Wrap attributes with the proper resource key for API requests
|
248
|
+
# Example: CF::Contacts::Tag -> { contacts_tag: attributes }
|
249
|
+
resource_key = generate_resource_key
|
250
|
+
{ resource_key => attributes }
|
251
|
+
end
|
252
|
+
|
253
|
+
def generate_resource_key
|
254
|
+
# Generate the resource key from class name
|
255
|
+
# CF::Contact -> :contact
|
256
|
+
# CF::Contacts::Tag -> :contacts_tag
|
257
|
+
# CF::Courses::LessonCompletion -> :courses_lesson_completion
|
258
|
+
|
259
|
+
parts = name.split("::")[1..-1] # Remove "CF"
|
260
|
+
|
261
|
+
# Convert each part to snake_case
|
262
|
+
# For namespaces (all but last), keep plural
|
263
|
+
# For resource (last), make singular
|
264
|
+
key_parts = parts.map.with_index do |part, index|
|
265
|
+
snake_case = part.gsub(/([A-Z])/, "_\\1").downcase.gsub(/^_/, "")
|
266
|
+
|
267
|
+
if index == parts.length - 1
|
268
|
+
# Last part is the resource - make it singular
|
269
|
+
singularize(snake_case)
|
270
|
+
else
|
271
|
+
# Namespace parts - keep as-is (should already be plural)
|
272
|
+
snake_case
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
key_parts.join("_").to_sym
|
277
|
+
end
|
278
|
+
|
279
|
+
def singularize(word)
|
280
|
+
# Simple singularization rules
|
281
|
+
return word unless word.end_with?('s')
|
282
|
+
|
283
|
+
case word
|
284
|
+
when /ies$/
|
285
|
+
word.sub(/ies$/, "y")
|
286
|
+
when /ses$/, /ches$/, /xes$/, /zes$/
|
287
|
+
word.sub(/es$/, "")
|
288
|
+
when /s$/
|
289
|
+
word.sub(/s$/, "")
|
290
|
+
else
|
291
|
+
word
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def special_workspace_pattern(parts)
|
296
|
+
# Map special cases where resources are under /workspaces but have different namespace
|
297
|
+
# Returns the pattern info if it matches, nil otherwise
|
298
|
+
return nil if parts.length != 2
|
299
|
+
|
300
|
+
namespace = parts[0].downcase
|
301
|
+
resource = parts[1].downcase
|
302
|
+
|
303
|
+
# These patterns require workspace_id despite their namespace
|
304
|
+
workspace_nested_patterns = {
|
305
|
+
["contacts", "tag"] => "contacts/tags",
|
306
|
+
["orders", "tag"] => "orders/tags",
|
307
|
+
["funnels", "tag"] => "funnels/tags",
|
308
|
+
["products", "tag"] => "products/tags",
|
309
|
+
["products", "collection"] => "products/collections"
|
310
|
+
}
|
311
|
+
|
312
|
+
# Special handling for Orders::Invoice - it can be both workspace and order nested
|
313
|
+
# We'll treat it as workspace-nested ONLY when no order_id is provided
|
314
|
+
if namespace == "orders" && resource == "invoice"
|
315
|
+
return nil # Let normal logic handle it
|
316
|
+
end
|
317
|
+
|
318
|
+
pattern_key = [namespace, resource]
|
319
|
+
if workspace_nested_patterns.key?(pattern_key)
|
320
|
+
{ path_suffix: workspace_nested_patterns[pattern_key] }
|
321
|
+
else
|
322
|
+
nil
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def build_special_workspace_path(pattern_info, params)
|
327
|
+
# Build path for special workspace-nested resources
|
328
|
+
segments = ["workspaces"]
|
329
|
+
|
330
|
+
# Get workspace_id from params or config
|
331
|
+
workspace_id = params[:workspace_id] || get_default_parent_id(:workspace_id)
|
332
|
+
|
333
|
+
if workspace_id
|
334
|
+
segments << workspace_id.to_s
|
335
|
+
else
|
336
|
+
raise CF::ConfigurationError, "workspace_id is required. Provide it as a parameter or set it in configuration."
|
337
|
+
end
|
338
|
+
|
339
|
+
# Add the specific path suffix (e.g., "contacts/tags")
|
340
|
+
segments << pattern_info[:path_suffix]
|
341
|
+
|
342
|
+
segments
|
343
|
+
end
|
344
|
+
|
345
|
+
def pluralize(word)
|
346
|
+
# Simple pluralization rules
|
347
|
+
# Don't pluralize words that are already plural
|
348
|
+
return word if word.end_with?('s') && !word.end_with?('ss')
|
349
|
+
|
350
|
+
case word
|
351
|
+
when /y$/
|
352
|
+
word.sub(/y$/, "ies")
|
353
|
+
when /sh$/, /ch$/, /x$/, /z$/
|
354
|
+
"#{word}es"
|
355
|
+
else
|
356
|
+
"#{word}s"
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# Dynamic class generation for all endpoints
|
363
|
+
# TODO: This can be more "dynamic" by using the naming conventions.
|
364
|
+
def self.generate_resource_classes!
|
365
|
+
# Define all the resource classes based on ENDPOINTS.md
|
366
|
+
# Following convention: CF::NamespaceS(plural)::Resource(singular)
|
367
|
+
|
368
|
+
# Top-level resources
|
369
|
+
define_resource_class("CF::Teams") # Collection
|
370
|
+
define_resource_class("CF::Team") # Individual resource
|
371
|
+
define_resource_class("CF::Users") # Collection
|
372
|
+
define_resource_class("CF::User") # Individual resource
|
373
|
+
|
374
|
+
# Workspace nested resources
|
375
|
+
define_resource_class("CF::Teams::Workspace") # Nested under teams
|
376
|
+
define_resource_class("CF::Workspaces") # Collection
|
377
|
+
define_resource_class("CF::Workspace") # Individual resource
|
378
|
+
define_resource_class("CF::Workspaces::Contact") # Nested under workspaces
|
379
|
+
define_resource_class("CF::Contact") # Individual resource
|
380
|
+
# FIXME: Instead of this, these need to be actions.
|
381
|
+
define_resource_class("CF::Contacts::GdprDestroy")
|
382
|
+
define_resource_class("CF::Workspaces::Contacts::Upsert")
|
383
|
+
|
384
|
+
# Contact Tags
|
385
|
+
define_resource_class("CF::Contacts::AppliedTag")
|
386
|
+
define_resource_class("CF::Contacts::Tag") # This maps to /workspaces/{id}/contacts/tags
|
387
|
+
|
388
|
+
# Courses
|
389
|
+
define_resource_class("CF::Workspaces::Course")
|
390
|
+
define_resource_class("CF::Courses")
|
391
|
+
define_resource_class("CF::Course")
|
392
|
+
define_resource_class("CF::Courses::Enrollment")
|
393
|
+
define_resource_class("CF::Courses::Section")
|
394
|
+
define_resource_class("CF::Courses::Sections::Lesson")
|
395
|
+
define_resource_class("CF::Courses::Lesson")
|
396
|
+
define_resource_class("CF::Courses::LessonCompletion")
|
397
|
+
|
398
|
+
# Forms
|
399
|
+
define_resource_class("CF::Workspaces::Form")
|
400
|
+
define_resource_class("CF::Forms")
|
401
|
+
define_resource_class("CF::Form")
|
402
|
+
define_resource_class("CF::Forms::FieldSet")
|
403
|
+
define_resource_class("CF::Forms::FieldSets::Field")
|
404
|
+
define_resource_class("CF::Forms::Field")
|
405
|
+
define_resource_class("CF::Forms::FieldSets::Fields::Reorder")
|
406
|
+
define_resource_class("CF::Forms::Fields::Option")
|
407
|
+
define_resource_class("CF::Forms::Submission")
|
408
|
+
define_resource_class("CF::Workspaces::FormSubmission")
|
409
|
+
define_resource_class("CF::Forms::Submissions::Answer")
|
410
|
+
|
411
|
+
# Orders
|
412
|
+
define_resource_class("CF::Workspaces::Order")
|
413
|
+
define_resource_class("CF::Orders")
|
414
|
+
define_resource_class("CF::Order")
|
415
|
+
define_resource_class("CF::Orders::AppliedTag")
|
416
|
+
define_resource_class("CF::Workspaces::Orders::Tag")
|
417
|
+
define_resource_class("CF::Orders::Tag")
|
418
|
+
define_resource_class("CF::Orders::Invoice")
|
419
|
+
define_resource_class("CF::Workspaces::Orders::Invoice")
|
420
|
+
define_resource_class("CF::Orders::Transaction")
|
421
|
+
define_resource_class("CF::Workspaces::Orders::Invoices::Restock")
|
422
|
+
define_resource_class("CF::Orders::Invoices::Restock")
|
423
|
+
|
424
|
+
# Products
|
425
|
+
define_resource_class("CF::Workspaces::Product")
|
426
|
+
define_resource_class("CF::Products")
|
427
|
+
define_resource_class("CF::Product")
|
428
|
+
define_resource_class("CF::Products::Archive")
|
429
|
+
define_resource_class("CF::Products::Unarchive")
|
430
|
+
define_resource_class("CF::Workspaces::Products::Collection")
|
431
|
+
define_resource_class("CF::Products::Collection")
|
432
|
+
define_resource_class("CF::Products::Price")
|
433
|
+
define_resource_class("CF::Products::Variant")
|
434
|
+
define_resource_class("CF::Workspaces::Products::Tag")
|
435
|
+
define_resource_class("CF::Products::Tag")
|
436
|
+
|
437
|
+
# Fulfillments
|
438
|
+
define_resource_class("CF::Workspaces::Fulfillment")
|
439
|
+
define_resource_class("CF::Fulfillments")
|
440
|
+
define_resource_class("CF::Fulfillment")
|
441
|
+
define_resource_class("CF::Fulfillments::Cancel")
|
442
|
+
define_resource_class("CF::Workspaces::Fulfillments::Location")
|
443
|
+
define_resource_class("CF::Fulfillments::Location")
|
444
|
+
|
445
|
+
# Funnels
|
446
|
+
define_resource_class("CF::Workspaces::Funnel")
|
447
|
+
define_resource_class("CF::Funnels")
|
448
|
+
define_resource_class("CF::Funnel")
|
449
|
+
define_resource_class("CF::Workspaces::Funnels::Tag")
|
450
|
+
define_resource_class("CF::Funnels::Tag")
|
451
|
+
|
452
|
+
# Pages
|
453
|
+
define_resource_class("CF::Workspaces::Page")
|
454
|
+
define_resource_class("CF::Pages")
|
455
|
+
define_resource_class("CF::Page")
|
456
|
+
|
457
|
+
# Images
|
458
|
+
define_resource_class("CF::Workspaces::Image")
|
459
|
+
define_resource_class("CF::Images")
|
460
|
+
define_resource_class("CF::Image")
|
461
|
+
|
462
|
+
# Sales
|
463
|
+
define_resource_class("CF::Workspaces::Sales::Pipeline")
|
464
|
+
define_resource_class("CF::Sales::Pipelines")
|
465
|
+
define_resource_class("CF::Sales::Pipeline")
|
466
|
+
define_resource_class("CF::Sales::Pipelines::Stage")
|
467
|
+
define_resource_class("CF::Workspaces::Sales::Opportunity")
|
468
|
+
define_resource_class("CF::Sales::Opportunities")
|
469
|
+
define_resource_class("CF::Sales::Opportunity")
|
470
|
+
define_resource_class("CF::Sales::Opportunities::Note")
|
471
|
+
|
472
|
+
# Shipping
|
473
|
+
define_resource_class("CF::Workspaces::Shipping::Profile")
|
474
|
+
define_resource_class("CF::Shipping::Profiles")
|
475
|
+
define_resource_class("CF::Shipping::Profile")
|
476
|
+
define_resource_class("CF::Shipping::Profiles::LocationGroup")
|
477
|
+
define_resource_class("CF::Shipping::LocationGroups::Zone")
|
478
|
+
define_resource_class("CF::Shipping::Zones::Rate")
|
479
|
+
define_resource_class("CF::Workspaces::Shipping::Rates::Name")
|
480
|
+
define_resource_class("CF::Shipping::Rates::Name")
|
481
|
+
define_resource_class("CF::Workspaces::Shipping::Package")
|
482
|
+
define_resource_class("CF::Shipping::Packages")
|
483
|
+
define_resource_class("CF::Shipping::Package")
|
484
|
+
|
485
|
+
# Stores
|
486
|
+
define_resource_class("CF::Workspaces::Store")
|
487
|
+
define_resource_class("CF::Stores")
|
488
|
+
define_resource_class("CF::Store")
|
489
|
+
|
490
|
+
# Themes & Styles
|
491
|
+
define_resource_class("CF::Workspaces::Theme")
|
492
|
+
define_resource_class("CF::Themes")
|
493
|
+
define_resource_class("CF::Theme")
|
494
|
+
define_resource_class("CF::Workspaces::Style")
|
495
|
+
define_resource_class("CF::Styles")
|
496
|
+
|
497
|
+
# Webhooks
|
498
|
+
define_resource_class("CF::Workspaces::Webhooks::Outgoing::Endpoint")
|
499
|
+
define_resource_class("CF::Webhooks::Outgoing::Endpoints")
|
500
|
+
define_resource_class("CF::Webhooks::Outgoing::Endpoint")
|
501
|
+
define_resource_class("CF::Workspaces::Webhooks::Outgoing::Event")
|
502
|
+
define_resource_class("CF::Webhooks::Outgoing::Events")
|
503
|
+
define_resource_class("CF::Webhooks::Outgoing::Event")
|
504
|
+
end
|
505
|
+
|
506
|
+
private
|
507
|
+
|
508
|
+
def self.define_resource_class(class_name)
|
509
|
+
# Create nested modules if they don't exist
|
510
|
+
parts = class_name.split("::")
|
511
|
+
current_module = Object
|
512
|
+
|
513
|
+
parts[0..-2].each do |part|
|
514
|
+
unless current_module.const_defined?(part)
|
515
|
+
current_module.const_set(part, Module.new)
|
516
|
+
end
|
517
|
+
current_module = current_module.const_get(part)
|
518
|
+
end
|
519
|
+
|
520
|
+
# Create the final class
|
521
|
+
class_name_part = parts.last
|
522
|
+
unless current_module.const_defined?(class_name_part)
|
523
|
+
current_module.const_set(class_name_part, Class.new(Base))
|
524
|
+
end
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
# Generate all resource classes when this file is loaded
|
530
|
+
CF::Resources.generate_resource_classes!
|
data/lib/cf/version.rb
ADDED
data/lib/cf.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "cf/version"
|
4
|
+
require_relative "cf/configuration"
|
5
|
+
require_relative "cf/client"
|
6
|
+
require_relative "cf/errors"
|
7
|
+
require_relative "cf/auth"
|
8
|
+
require_relative "cf/logger"
|
9
|
+
require_relative "cf/http/client"
|
10
|
+
require_relative "cf/http/net_http_adapter"
|
11
|
+
require_relative "cf/resources/base"
|
12
|
+
|
13
|
+
module CF
|
14
|
+
class << self
|
15
|
+
def configure
|
16
|
+
yield(configuration)
|
17
|
+
end
|
18
|
+
|
19
|
+
def configuration
|
20
|
+
@configuration ||= Configuration.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def client
|
24
|
+
@client ||= Client.new(configuration)
|
25
|
+
end
|
26
|
+
|
27
|
+
def reset!
|
28
|
+
@configuration = nil
|
29
|
+
@client = nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: clickfunnels-ruby-sdk
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rich Steinmetz
|
8
|
+
- Claude Code
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-06-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '13.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '13.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest-reporters
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: webmock
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
description: A simple, dynamic Ruby SDK for the ClickFunnels API with support for
|
98
|
+
all endpoints and operations.
|
99
|
+
email:
|
100
|
+
- hey@richsteinmetz.com
|
101
|
+
executables: []
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- CHANGELOG.md
|
106
|
+
- LICENSE
|
107
|
+
- README.md
|
108
|
+
- lib/cf.rb
|
109
|
+
- lib/cf/auth.rb
|
110
|
+
- lib/cf/client.rb
|
111
|
+
- lib/cf/configuration.rb
|
112
|
+
- lib/cf/errors.rb
|
113
|
+
- lib/cf/http/client.rb
|
114
|
+
- lib/cf/http/net_http_adapter.rb
|
115
|
+
- lib/cf/logger.rb
|
116
|
+
- lib/cf/resources/base.rb
|
117
|
+
- lib/cf/version.rb
|
118
|
+
homepage: https://github.com/RichStone/clickfunnels-ruby-sdk
|
119
|
+
licenses:
|
120
|
+
- MIT
|
121
|
+
metadata:
|
122
|
+
homepage_uri: https://github.com/RichStone/clickfunnels-ruby-sdk
|
123
|
+
source_code_uri: https://github.com/RichStone/clickfunnels-ruby-sdk
|
124
|
+
changelog_uri: https://github.com/RichStone/clickfunnels-ruby-sdk/blob/main/CHANGELOG.md
|
125
|
+
rdoc_options: []
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: 3.1.6
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
requirements: []
|
139
|
+
rubygems_version: 3.6.5
|
140
|
+
specification_version: 4
|
141
|
+
summary: Ruby SDK for ClickFunnels API
|
142
|
+
test_files: []
|