ruby_api_pack_core 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.
data/README.md ADDED
@@ -0,0 +1,281 @@
1
+ # Ruby API Pack Core
2
+
3
+ ## Repository Snapshot
4
+
5
+ | Field | Value |
6
+ |-------|-------|
7
+ | Project team | `project-ruby` |
8
+ | Repository role | Shared HTTP client foundation for all `ruby_api_pack_*` gems |
9
+ | Package/artifact | `ruby_api_pack_core` |
10
+ | Current version/status | 0.1.0 |
11
+
12
+ ## Standard Workflow
13
+
14
+ 1. Read [AGENTS.md](AGENTS.md), then the agent-specific guide for the task.
15
+ 2. Check [TODO.md](TODO.md) and [ROADMAP.md](ROADMAP.md) for current scope.
16
+ 3. Make the smallest repo-local change that satisfies the task.
17
+ 4. Run `bundle exec rspec`, `bundle exec rubocop`, and
18
+ `gem build ruby_api_pack_core.gemspec` when validation is required or
19
+ practical.
20
+ 5. Update docs and changelog history only when behavior, public contracts, or
21
+ release-relevant metadata changed.
22
+
23
+ ## Documentation Map
24
+
25
+ | Guide | Path |
26
+ |-------|------|
27
+ | Agent rules | [AGENTS.md](AGENTS.md) |
28
+ | Claude Code | [CLAUDE.md](CLAUDE.md) |
29
+ | Codex | [CODEX.md](CODEX.md) |
30
+ | Copilot | [COPILOT.md](COPILOT.md) |
31
+ | Jules | [JULES.md](JULES.md) |
32
+ | Roadmap | [ROADMAP.md](ROADMAP.md) |
33
+ | Todo | [TODO.md](TODO.md) |
34
+ | Changelog | [CHANGELOG.md](CHANGELOG.md) |
35
+ | Security | [SECURITY.md](SECURITY.md) |
36
+ | Code of Conduct | [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) |
37
+ | Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) |
38
+
39
+ [![Gem Version](https://img.shields.io/gem/v/ruby_api_pack_core.svg)](https://rubygems.org/gems/ruby_api_pack_core)
40
+ [![RSpec](https://github.com/phcdevworks/ruby_api_pack_core/actions/workflows/test.yml/badge.svg)](https://github.com/phcdevworks/ruby_api_pack_core/actions/workflows/test.yml)
41
+ [![License](https://img.shields.io/github/license/phcdevworks/ruby_api_pack_core.svg)](MIT-LICENSE)
42
+
43
+ `ruby_api_pack_core` is the shared HTTP client foundation consumed by every
44
+ `ruby_api_pack_*` gem (ActiveCampaign, Cloudways, WordPress, and any future
45
+ vendor pack). It owns the connection wrapper's HTTParty/Oj plumbing, a
46
+ generic response-shape validator, and the `configure`/`configuration`
47
+ singleton pattern, so each vendor-specific gem only has to implement its own
48
+ authentication headers, `Configuration` fields, and resource endpoint
49
+ classes — instead of every gem reinventing the same request/response
50
+ boilerplate.
51
+
52
+ [Contributing](CONTRIBUTING.md) | [Code of Conduct](CODE_OF_CONDUCT.md) |
53
+ [Changelog](CHANGELOG.md) | [Roadmap](ROADMAP.md) |
54
+ [Security Policy](SECURITY.md) | [AI Guide](AGENTS.md)
55
+
56
+ ## Source of Truth
57
+
58
+ The gem's public behavior is defined by its connection base class, response
59
+ validator, configurable mixin, and specs. Keep those surfaces aligned — every
60
+ `ruby_api_pack_*` gem depends on this contract staying stable.
61
+
62
+ | Layer | Path | Rule |
63
+ | --- | --- | --- |
64
+ | Gem entry point | `lib/ruby_api_pack_core.rb` | Loads the connection, validator, and configurable surfaces |
65
+ | Connection base | `lib/ruby_api_pack_core/connection/base.rb` | Template-method HTTParty/Oj connection wrapper; subclasses implement `#auth_headers` |
66
+ | Response validator | `lib/ruby_api_pack_core/handlers/response_validator.rb` | Generic `expected_type: :array`/`:hash` response-shape validation |
67
+ | Configurable | `lib/ruby_api_pack_core/configurable.rb` | Shared `configure`/`configuration` singleton mixin |
68
+ | Version | `lib/ruby_api_pack_core/version.rb` | Gem release version |
69
+ | Specs | `spec/` | Contract and regression coverage, exercised against dummy subclasses/modules |
70
+
71
+ After behavior changes, run:
72
+
73
+ ```bash
74
+ bundle exec rspec
75
+ bundle exec rubocop
76
+ gem build ruby_api_pack_core.gemspec
77
+ ```
78
+
79
+ ## What This Gem Owns
80
+
81
+ - A template-method `RubyApiPackCore::Connection::Base` class providing
82
+ `api_get`/`api_post`/`api_put`/`api_delete`, URL building, `200..299`
83
+ status handling, content-type-checked JSON parsing via `Oj`, and
84
+ descriptive error messages
85
+ - `RubyApiPackCore::Handlers::ResponseValidator`, a generic response-shape
86
+ validator (`expected_type: :array`/`:hash`) with Rails-aware error logging
87
+ - `RubyApiPackCore::Configurable`, a mixin providing the standard
88
+ `MyGem.configure { |c| ... }` / `MyGem.configuration` singleton pattern
89
+ - RSpec coverage for all of the above, exercised against synthetic dummy
90
+ subclasses and modules
91
+
92
+ ## What This Gem Does Not Own
93
+
94
+ - Any vendor-specific API knowledge — API tokens, OAuth flows, Basic Auth,
95
+ endpoint paths, or resource classes belong in the consuming
96
+ `ruby_api_pack_*` gem
97
+ - Host application models, persistence, background jobs, or authorization
98
+ - User-facing Rails controllers or UI components
99
+ - Storage of production credentials or API tokens
100
+
101
+ ## Installation
102
+
103
+ Add the gem to your application's Gemfile:
104
+
105
+ ```ruby
106
+ gem "ruby_api_pack_core"
107
+ ```
108
+
109
+ Install dependencies:
110
+
111
+ ```bash
112
+ bundle install
113
+ ```
114
+
115
+ Or install the gem directly:
116
+
117
+ ```bash
118
+ gem install ruby_api_pack_core
119
+ ```
120
+
121
+ In practice, most consumers won't install this gem directly — it's a runtime
122
+ dependency declared by each `ruby_api_pack_*` gem's own gemspec.
123
+
124
+ ## Building a New API Pack on `ruby_api_pack_core`
125
+
126
+ A new vendor API pack gem needs three pieces built on top of this
127
+ foundation:
128
+
129
+ ### 1. Configuration
130
+
131
+ ```ruby
132
+ module RubyApiPackExample
133
+ class Configuration
134
+ attr_accessor :api_url, :api_token
135
+
136
+ def initialize
137
+ @api_url = 'https://api.example.com/v1'
138
+ @api_token = nil
139
+ end
140
+ end
141
+ end
142
+
143
+ module RubyApiPackExample
144
+ extend RubyApiPackCore::Configurable
145
+
146
+ def self.configuration_class
147
+ Configuration
148
+ end
149
+ end
150
+ ```
151
+
152
+ This gives consumers the standard pattern:
153
+
154
+ ```ruby
155
+ RubyApiPackExample.configure do |config|
156
+ config.api_url = ENV.fetch("EXAMPLE_API_URL")
157
+ config.api_token = ENV.fetch("EXAMPLE_API_TOKEN")
158
+ end
159
+ ```
160
+
161
+ ### 2. Connection wrapper
162
+
163
+ Subclass `RubyApiPackCore::Connection::Base` and implement only
164
+ `#auth_headers` — everything else (URL building, verb dispatch, status
165
+ handling, JSON parsing) is inherited:
166
+
167
+ ```ruby
168
+ module RubyApiPackExample
169
+ module Connection
170
+ class ExampleConnect < RubyApiPackCore::Connection::Base
171
+ private
172
+
173
+ def auth_headers
174
+ { 'Api-Token' => RubyApiPackExample.configuration.api_token }
175
+ end
176
+ end
177
+ end
178
+ end
179
+ ```
180
+
181
+ ### 3. Resource classes
182
+
183
+ Extend `RubyApiPackCore::Handlers::ResponseValidator` and follow the
184
+ `ENDPOINT` constant + `class << self` + private `connection(id = nil)`
185
+ factory pattern used by every existing `ruby_api_pack_*` resource class:
186
+
187
+ ```ruby
188
+ module RubyApiPackExample
189
+ module Api
190
+ class Widgets
191
+ extend RubyApiPackCore::Handlers::ResponseValidator
192
+
193
+ ENDPOINT = '/widgets'
194
+
195
+ class << self
196
+ def widget_list
197
+ validate_response(connection.api_get, expected_type: :array)
198
+ end
199
+
200
+ def widget_by_id(id)
201
+ validate_response(connection(id).api_get, expected_type: :hash)
202
+ end
203
+
204
+ private
205
+
206
+ def connection(id = nil)
207
+ path = id ? "#{ENDPOINT}/#{id}" : ENDPOINT
208
+ Connection::ExampleConnect.new(RubyApiPackExample.configuration.api_url, path)
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ ```
215
+
216
+ This is the same architecture used by `ruby_api_pack_active_campaign`,
217
+ `ruby_api_pack_cloudways`, and `ruby_api_pack_wordpress` — see those gems for
218
+ worked, real-world examples.
219
+
220
+ ## Error Handling
221
+
222
+ `Connection::Base` raises a plain `RuntimeError` (via `raise "..."`) for
223
+ non-2xx responses, unparseable JSON, or an unexpected content type. This
224
+ matches the existing behavior of every `ruby_api_pack_*` gem. A typed error
225
+ class hierarchy is tracked in [ROADMAP.md](ROADMAP.md) as a future
226
+ enhancement, but is not yet implemented — do not assume specific error
227
+ classes are raised.
228
+
229
+ ## Development
230
+
231
+ Install dependencies:
232
+
233
+ ```bash
234
+ bundle install
235
+ ```
236
+
237
+ Run the test suite:
238
+
239
+ ```bash
240
+ bundle exec rspec
241
+ ```
242
+
243
+ Run style checks:
244
+
245
+ ```bash
246
+ bundle exec rubocop
247
+ ```
248
+
249
+ Build the gem locally:
250
+
251
+ ```bash
252
+ gem build ruby_api_pack_core.gemspec
253
+ ```
254
+
255
+ ## Documentation and AI Guides
256
+
257
+ This repository follows the PHCDevworks documentation model used across the
258
+ Spectre and Rails/Ruby workspaces:
259
+
260
+ - `AGENTS.md` defines shared AI boundaries.
261
+ - `CLAUDE.md`, `CODEX.md`, `COPILOT.md`, and `JULES.md` define agent-specific
262
+ working rules.
263
+ - `.github/copilot-instructions.md` and `.github/codex-instructions.md` provide
264
+ GitHub-integrated assistant guidance.
265
+ - `CHANGELOG.md`, `ROADMAP.md`, and `TODO.md` keep release and planning context
266
+ visible.
267
+
268
+ ## Contributing
269
+
270
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, coding standards, pull
271
+ request expectations, and release hygiene.
272
+
273
+ ## Security
274
+
275
+ Please do not report vulnerabilities through public issues. Follow
276
+ [SECURITY.md](SECURITY.md) for responsible disclosure.
277
+
278
+ ## License
279
+
280
+ The gem is available as open source under the terms of the
281
+ [MIT License](MIT-LICENSE).
data/ROADMAP.md ADDED
@@ -0,0 +1,42 @@
1
+ # Roadmap
2
+
3
+ This roadmap tracks the direction for `ruby_api_pack_core` as the shared HTTP
4
+ client foundation for every `ruby_api_pack_*` gem. It is planning context, not
5
+ a release promise.
6
+
7
+ ## Current Focus
8
+
9
+ - Keep `Connection::Base`, `Handlers::ResponseValidator`, and `Configurable`
10
+ stable now that `ruby_api_pack_active_campaign`, `ruby_api_pack_cloudways`,
11
+ and `ruby_api_pack_wordpress` all depend on this gem.
12
+ - Keep this gem free of any vendor-specific knowledge.
13
+
14
+ ## Near-Term
15
+
16
+ - Confirm all three consuming gems pass their own validation gates against
17
+ the published `ruby_api_pack_core` version.
18
+ - Expand README examples showing how a new `ruby_api_pack_*` gem should
19
+ subclass `Connection::Base` and extend `Configurable`.
20
+ - Consider a typed error class hierarchy (e.g.
21
+ `RubyApiPackCore::Errors::HttpError`,
22
+ `RubyApiPackCore::Errors::ParseError`) shared by all consuming gems, since
23
+ every gem currently raises plain `RuntimeError`s independently.
24
+ - Confirm supported Ruby versions across CI and gem metadata.
25
+
26
+ ## Later
27
+
28
+ - Consider an optional retry/backoff hook on `Connection::Base` if more than
29
+ one consuming gem needs rate-limit handling (currently only
30
+ `ruby_api_pack_cloudways` retries on rate limits, implemented locally).
31
+ - Add a generator or documented scaffold for creating a new `ruby_api_pack_*`
32
+ gem from this foundation.
33
+ - Evaluate whether `Handlers::ResponseValidator`'s `expected_key` unwrapping
34
+ style (used historically by `ruby_api_pack_cloudways`) should be offered as
35
+ an alternative mode alongside `expected_type`.
36
+
37
+ ## Out of Scope
38
+
39
+ - Any vendor-specific API knowledge (tokens, endpoint paths, resource classes)
40
+ - Host application data models or persistence
41
+ - User-facing Rails controllers or views
42
+ - Storage or management of production secrets
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/SECURITY.md ADDED
@@ -0,0 +1,46 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ PHCDevworks applies security fixes to the current release line of this gem.
6
+ Please use the latest published version of `ruby_api_pack_core` whenever
7
+ possible.
8
+
9
+ ## Reporting a Vulnerability
10
+
11
+ Do not open a public issue for security problems.
12
+
13
+ Use GitHub Security Advisories for this repository whenever possible. If that
14
+ is not available, contact the maintainers through GitHub.
15
+
16
+ Include:
17
+
18
+ 1. A clear description of the issue and its impact
19
+ 2. Steps to reproduce or a proof of concept
20
+ 3. Affected versions, if known
21
+ 4. Any suggested mitigation
22
+
23
+ ## Response Expectations
24
+
25
+ 1. We aim to acknowledge reports within 48 hours.
26
+ 2. We aim to provide an initial assessment within 5 business days.
27
+ 3. We will coordinate disclosure timing with the reporter when possible.
28
+
29
+ ## Security Guidance
30
+
31
+ - Keep Ruby, Bundler, HTTParty, and development dependencies up to date.
32
+ - This gem carries no vendor credentials of its own — API tokens, OAuth
33
+ secrets, and Basic Auth credentials live in the consuming
34
+ `ruby_api_pack_*` gems' configuration objects, not here.
35
+ - Because every `ruby_api_pack_*` gem depends on this one, a vulnerability
36
+ here has broader blast radius than a vulnerability in a single vendor pack
37
+ — treat reports against `Connection::Base` and
38
+ `Handlers::ResponseValidator` with correspondingly higher urgency.
39
+ - Review automated dependency updates and advisories before release.
40
+ - Specs use synthetic fixtures and mocked HTTParty responses only — never
41
+ record real vendor API traffic against this repository.
42
+
43
+ ## Contact
44
+
45
+ For non-sensitive security questions, open an issue or discussion in this
46
+ repository.
data/TODO.md ADDED
@@ -0,0 +1,32 @@
1
+ # TODO
2
+
3
+ ## Documentation
4
+
5
+ - [x] Standardize README structure.
6
+ - [x] Add AI operating guides.
7
+ - [x] Standardize community health docs.
8
+ - [x] Add changelog and roadmap.
9
+ - [ ] Add a worked example of scaffolding a new `ruby_api_pack_*` gem from
10
+ this foundation.
11
+
12
+ ## Ruby Gem
13
+
14
+ - [x] Extract shared connection wrapper, response validator, and
15
+ configurable pattern from `ruby_api_pack_active_campaign`,
16
+ `ruby_api_pack_cloudways`, and `ruby_api_pack_wordpress`.
17
+ - [ ] Consider a custom error class hierarchy for HTTP and parse failures,
18
+ shared across all consuming gems.
19
+ - [ ] Consider timeout configuration for HTTParty requests.
20
+ - [ ] Evaluate whether `Handlers::ResponseValidator` should support
21
+ Cloudways's historical `expected_key` unwrapping mode alongside
22
+ `expected_type`.
23
+
24
+ ## Requested by Downstream
25
+
26
+ (none yet)
27
+
28
+ ## Release
29
+
30
+ - [x] Confirm gemspec metadata links point to the repository and changelog.
31
+ - [x] Document RubyGems release steps.
32
+ - [x] Confirm supported Ruby versions in CI match the gemspec.
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyApiPackCore
4
+ # Extend a top-level gem module with this to get the standard
5
+ # `MyGem.configure { |c| ... }` / `MyGem.configuration` singleton pattern.
6
+ #
7
+ # The extending module must implement `.configuration_class`, returning the
8
+ # Configuration class to instantiate, e.g.:
9
+ #
10
+ # module MyGem
11
+ # extend RubyApiPackCore::Configurable
12
+ #
13
+ # def self.configuration_class
14
+ # Configuration
15
+ # end
16
+ # end
17
+ module Configurable
18
+ attr_accessor :configuration
19
+
20
+ def configure
21
+ self.configuration ||= configuration_class.new
22
+ yield(configuration)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+ require 'oj'
5
+
6
+ module RubyApiPackCore
7
+ module Connection
8
+ # Shared HTTParty + Oj request/response plumbing for API pack connection
9
+ # wrappers. Subclasses provide the vendor-specific piece by implementing
10
+ # `#auth_headers` (a Hash merged into every request's headers). Everything
11
+ # else -- URL building, verb dispatch, status handling, JSON parsing, and
12
+ # error messages -- is identical across packs.
13
+ class Base
14
+ attr_reader :api_url_base, :api_path
15
+
16
+ def initialize(api_url_base, api_path)
17
+ @api_url_base = api_url_base
18
+ @api_path = api_path
19
+ end
20
+
21
+ def api_get(params = {})
22
+ response = HTTParty.get(
23
+ full_url,
24
+ headers: auth_headers,
25
+ query: params,
26
+ ssl_version: :TLSv1_2
27
+ )
28
+ handle_response(response)
29
+ end
30
+
31
+ def api_post(params = {})
32
+ response = HTTParty.post(
33
+ full_url,
34
+ headers: auth_headers.merge('Content-Type' => 'application/json'),
35
+ body: params.to_json,
36
+ ssl_version: :TLSv1_2
37
+ )
38
+ handle_response(response)
39
+ end
40
+
41
+ def api_put(params = {})
42
+ response = HTTParty.put(
43
+ full_url,
44
+ headers: auth_headers.merge('Content-Type' => 'application/json'),
45
+ body: params.to_json,
46
+ ssl_version: :TLSv1_2
47
+ )
48
+ handle_response(response)
49
+ end
50
+
51
+ def api_delete(params = {})
52
+ response = HTTParty.delete(
53
+ full_url,
54
+ headers: auth_headers,
55
+ query: params,
56
+ ssl_version: :TLSv1_2
57
+ )
58
+ handle_response(response)
59
+ end
60
+
61
+ private
62
+
63
+ # Subclasses must implement this, returning a Hash of auth headers
64
+ # (e.g. an API-token header, an OAuth bearer token, or Basic auth).
65
+ def auth_headers
66
+ raise NotImplementedError, "#{self.class} must implement #auth_headers"
67
+ end
68
+
69
+ def full_url
70
+ "#{@api_url_base}#{@api_path}"
71
+ end
72
+
73
+ def handle_response(response)
74
+ case response.code
75
+ when 200..299
76
+ parse_response(response)
77
+ else
78
+ raise "Error: Received status #{response.code} - #{response.body}"
79
+ end
80
+ end
81
+
82
+ def parse_response(response)
83
+ return {} if response.body.nil? || response.body.empty?
84
+
85
+ content_type = response.headers&.fetch('content-type', nil)
86
+ raise "Unexpected response: #{response.body}" unless content_type&.include?('application/json')
87
+
88
+ Oj.load(response.body, mode: :strict)
89
+ rescue Oj::ParseError => e
90
+ raise "Error parsing response: #{e.message}. Raw body: #{response.body}"
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyApiPackCore
4
+ module Handlers
5
+ module ResponseValidator
6
+ def validate_response(response, expected_type: :any)
7
+ case expected_type
8
+ when :array
9
+ raise "Expected an Array, got #{response.class}: #{response.inspect}" unless response.is_a?(Array)
10
+ when :hash
11
+ raise "Expected a Hash, got #{response.class}: #{response.inspect}" unless response.is_a?(Hash)
12
+ end
13
+
14
+ response
15
+ rescue StandardError => e
16
+ log_error("Error validating response: #{e.message}")
17
+ raise "An error occurred while processing the response: #{e.message}"
18
+ end
19
+
20
+ private
21
+
22
+ def log_error(message)
23
+ if defined?(Rails)
24
+ Rails.logger.error(message)
25
+ else
26
+ puts "[ERROR] #{message}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyApiPackCore
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+ require 'oj'
5
+
6
+ require_relative 'ruby_api_pack_core/version'
7
+ require_relative 'ruby_api_pack_core/configurable'
8
+ require_relative 'ruby_api_pack_core/handlers/response_validator'
9
+ require_relative 'ruby_api_pack_core/connection/base'
10
+
11
+ module RubyApiPackCore
12
+ end
@@ -0,0 +1,8 @@
1
+ {
2
+ "folders": [
3
+ {
4
+ "path": "."
5
+ }
6
+ ],
7
+ "settings": {}
8
+ }