@workos/oagen-emitters 0.2.1 → 0.3.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.
Files changed (103) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +8 -0
  4. package/README.md +129 -0
  5. package/dist/index.d.mts +10 -1
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +11893 -3226
  8. package/dist/index.mjs.map +1 -1
  9. package/docs/sdk-architecture/go.md +338 -0
  10. package/docs/sdk-architecture/php.md +315 -0
  11. package/docs/sdk-architecture/python.md +511 -0
  12. package/oagen.config.ts +298 -2
  13. package/package.json +9 -5
  14. package/scripts/generate-php.js +13 -0
  15. package/scripts/git-push-with-published-oagen.sh +21 -0
  16. package/smoke/sdk-go.ts +116 -42
  17. package/smoke/sdk-php.ts +28 -26
  18. package/smoke/sdk-python.ts +5 -2
  19. package/src/go/client.ts +141 -0
  20. package/src/go/enums.ts +196 -0
  21. package/src/go/fixtures.ts +212 -0
  22. package/src/go/index.ts +81 -0
  23. package/src/go/manifest.ts +36 -0
  24. package/src/go/models.ts +254 -0
  25. package/src/go/naming.ts +191 -0
  26. package/src/go/resources.ts +827 -0
  27. package/src/go/tests.ts +751 -0
  28. package/src/go/type-map.ts +82 -0
  29. package/src/go/wrappers.ts +261 -0
  30. package/src/index.ts +3 -0
  31. package/src/node/client.ts +78 -115
  32. package/src/node/enums.ts +9 -0
  33. package/src/node/errors.ts +37 -232
  34. package/src/node/field-plan.ts +726 -0
  35. package/src/node/fixtures.ts +9 -1
  36. package/src/node/index.ts +2 -9
  37. package/src/node/models.ts +178 -21
  38. package/src/node/naming.ts +49 -111
  39. package/src/node/resources.ts +374 -364
  40. package/src/node/sdk-errors.ts +41 -0
  41. package/src/node/tests.ts +32 -12
  42. package/src/node/type-map.ts +4 -2
  43. package/src/node/utils.ts +13 -71
  44. package/src/node/wrappers.ts +151 -0
  45. package/src/php/client.ts +171 -0
  46. package/src/php/enums.ts +67 -0
  47. package/src/php/errors.ts +9 -0
  48. package/src/php/fixtures.ts +181 -0
  49. package/src/php/index.ts +96 -0
  50. package/src/php/manifest.ts +36 -0
  51. package/src/php/models.ts +310 -0
  52. package/src/php/naming.ts +298 -0
  53. package/src/php/resources.ts +561 -0
  54. package/src/php/tests.ts +533 -0
  55. package/src/php/type-map.ts +90 -0
  56. package/src/php/utils.ts +18 -0
  57. package/src/php/wrappers.ts +151 -0
  58. package/src/python/client.ts +337 -0
  59. package/src/python/enums.ts +313 -0
  60. package/src/python/fixtures.ts +196 -0
  61. package/src/python/index.ts +95 -0
  62. package/src/python/manifest.ts +38 -0
  63. package/src/python/models.ts +688 -0
  64. package/src/python/naming.ts +209 -0
  65. package/src/python/resources.ts +1322 -0
  66. package/src/python/tests.ts +1335 -0
  67. package/src/python/type-map.ts +93 -0
  68. package/src/python/wrappers.ts +191 -0
  69. package/src/shared/model-utils.ts +255 -0
  70. package/src/shared/naming-utils.ts +107 -0
  71. package/src/shared/non-spec-services.ts +54 -0
  72. package/src/shared/resolved-ops.ts +109 -0
  73. package/src/shared/wrapper-utils.ts +59 -0
  74. package/test/go/client.test.ts +92 -0
  75. package/test/go/enums.test.ts +132 -0
  76. package/test/go/errors.test.ts +9 -0
  77. package/test/go/models.test.ts +265 -0
  78. package/test/go/resources.test.ts +408 -0
  79. package/test/go/tests.test.ts +143 -0
  80. package/test/node/client.test.ts +18 -12
  81. package/test/node/enums.test.ts +2 -0
  82. package/test/node/errors.test.ts +2 -41
  83. package/test/node/models.test.ts +2 -0
  84. package/test/node/naming.test.ts +23 -0
  85. package/test/node/resources.test.ts +99 -69
  86. package/test/node/serializers.test.ts +3 -1
  87. package/test/node/type-map.test.ts +11 -0
  88. package/test/php/client.test.ts +94 -0
  89. package/test/php/enums.test.ts +173 -0
  90. package/test/php/errors.test.ts +9 -0
  91. package/test/php/models.test.ts +497 -0
  92. package/test/php/resources.test.ts +644 -0
  93. package/test/php/tests.test.ts +118 -0
  94. package/test/python/client.test.ts +200 -0
  95. package/test/python/enums.test.ts +228 -0
  96. package/test/python/errors.test.ts +16 -0
  97. package/test/python/manifest.test.ts +74 -0
  98. package/test/python/models.test.ts +716 -0
  99. package/test/python/resources.test.ts +617 -0
  100. package/test/python/tests.test.ts +202 -0
  101. package/src/node/common.ts +0 -273
  102. package/src/node/config.ts +0 -71
  103. package/src/node/serializers.ts +0 -746
@@ -0,0 +1,338 @@
1
+ # Go SDK Architecture
2
+
3
+ Scenario B (fresh) -- no backwards-compatibility constraints.
4
+ Reference: stripe/stripe-go for idiomatic Go patterns.
5
+
6
+ ## Architecture Overview
7
+
8
+ Single flat `workos` package. All types, services, and the client live in one package,
9
+ accessed as `workos.Organization`, `workos.NewClient(...)`, etc.
10
+
11
+ - **Client**: Instance-scoped via `NewClient(apiKey, ...Option)`
12
+ - **Services**: Unexported structs with exported methods, accessed as fields on Client
13
+ - **Models**: Exported structs with `json:"snake_case"` tags
14
+ - **Enums**: Typed `string` constants (no iota)
15
+ - **Params**: `*Params` structs with embedded `RequestOptions` for per-request overrides
16
+ - **Errors**: SDK-native error types implementing `error` interface
17
+ - **Pagination**: Iterator with `Next()` / `Current()` / `Err()`
18
+
19
+ ## Naming Conventions
20
+
21
+ | IR Name | Go Name | Context |
22
+ | -------------------------- | --------------------- | ---------------------------------------- |
23
+ | `Organization` (model) | `Organization` | Struct type |
24
+ | `organization` (file) | `organization.go` | File name |
25
+ | `listUsers` (method) | `ListUsers` | Exported method |
26
+ | `user_id` (field) | `UserID` | Struct field (PascalCase, acronym-aware) |
27
+ | `Status` (enum) | `Status` | Type declaration |
28
+ | `active` (enum value) | `StatusActive` | Const: `{TypeName}{PascalValue}` |
29
+ | `Organizations` (service) | `organizationService` | Unexported struct |
30
+ | `organizations` (accessor) | `Organizations()` | Client method returning service |
31
+
32
+ ### Acronym handling
33
+
34
+ Go convention preserves full-caps for common acronyms: `ID`, `URL`, `SSO`, `API`, `HTTP`,
35
+ `JWT`, `MFA`, `CORS`, `SAML`, `SCIM`, `RBAC`, `OAuth`, `OIDC`, `UUID`, `JSON`, `HTML`.
36
+
37
+ ## Type Mapping
38
+
39
+ | IR TypeRef | Go Type |
40
+ | ---------------------------- | --------------------------------- |
41
+ | `primitive:string` | `string` |
42
+ | `primitive:string:date` | `string` |
43
+ | `primitive:string:date-time` | `string` |
44
+ | `primitive:string:uuid` | `string` |
45
+ | `primitive:string:binary` | `[]byte` |
46
+ | `primitive:integer` | `int` |
47
+ | `primitive:number` | `float64` |
48
+ | `primitive:boolean` | `bool` |
49
+ | `primitive:unknown` | `interface{}` |
50
+ | `array` | `[]T` |
51
+ | `model` | `*ModelName` (pointer for nested) |
52
+ | `enum` | `EnumType` |
53
+ | `nullable` | `*T` (pointer) |
54
+ | `union` | `interface{}` |
55
+ | `map` | `map[string]T` |
56
+ | `literal:string` | `string` |
57
+ | `literal:null` | `interface{}` |
58
+
59
+ ## Model Pattern
60
+
61
+ ```go
62
+ // Organization represents an organization.
63
+ type Organization struct {
64
+ ID string `json:"id"`
65
+ Name string `json:"name"`
66
+ Domains []string `json:"domains"`
67
+ Metadata map[string]string `json:"metadata,omitempty"`
68
+ CreatedAt string `json:"created_at"`
69
+ UpdatedAt string `json:"updated_at"`
70
+ }
71
+ ```
72
+
73
+ - Required fields: value types (no pointer)
74
+ - Optional fields: pointer types with `omitempty`
75
+ - Nested models: always pointers
76
+ - JSON tags: snake_case matching the API wire format
77
+
78
+ ## Enum Pattern
79
+
80
+ ```go
81
+ // OrganizationStatus represents the status of an organization.
82
+ type OrganizationStatus string
83
+
84
+ const (
85
+ OrganizationStatusActive OrganizationStatus = "active"
86
+ OrganizationStatusInactive OrganizationStatus = "inactive"
87
+ )
88
+ ```
89
+
90
+ ## Resource/Service Pattern
91
+
92
+ ```go
93
+ type organizationService struct {
94
+ client *Client
95
+ }
96
+
97
+ // ListOrganizations lists all organizations.
98
+ func (s *organizationService) ListOrganizations(
99
+ ctx context.Context,
100
+ params *ListOrganizationsParams,
101
+ opts ...RequestOption,
102
+ ) *Iterator[Organization] {
103
+ // ...
104
+ }
105
+
106
+ // GetOrganization retrieves an organization by ID.
107
+ func (s *organizationService) GetOrganization(
108
+ ctx context.Context,
109
+ id string,
110
+ opts ...RequestOption,
111
+ ) (*Organization, error) {
112
+ // ...
113
+ }
114
+
115
+ // CreateOrganization creates a new organization.
116
+ func (s *organizationService) CreateOrganization(
117
+ ctx context.Context,
118
+ params *CreateOrganizationParams,
119
+ opts ...RequestOption,
120
+ ) (*Organization, error) {
121
+ // ...
122
+ }
123
+
124
+ // DeleteOrganization deletes an organization by ID.
125
+ func (s *organizationService) DeleteOrganization(
126
+ ctx context.Context,
127
+ id string,
128
+ opts ...RequestOption,
129
+ ) error {
130
+ // ...
131
+ }
132
+ ```
133
+
134
+ ### Parameter Structs
135
+
136
+ ```go
137
+ type CreateOrganizationParams struct {
138
+ Name string `json:"name"`
139
+ Domains []string `json:"domains,omitempty"`
140
+ Metadata map[string]string `json:"metadata,omitempty"`
141
+ }
142
+
143
+ type ListOrganizationsParams struct {
144
+ After *string `url:"after,omitempty"`
145
+ Before *string `url:"before,omitempty"`
146
+ Limit *int `url:"limit,omitempty"`
147
+ }
148
+ ```
149
+
150
+ ### Path parameters
151
+
152
+ Path parameters (IDs etc.) are positional function arguments, not part of params structs.
153
+
154
+ ## Client Pattern
155
+
156
+ ```go
157
+ // Client is the WorkOS API client.
158
+ type Client struct {
159
+ apiKey string
160
+ baseURL string
161
+ httpClient *http.Client
162
+ maxRetries int
163
+ // Service accessors
164
+ organizations *organizationService
165
+ users *userService
166
+ // ...
167
+ }
168
+
169
+ // NewClient creates a new WorkOS client.
170
+ func NewClient(apiKey string, opts ...ClientOption) *Client {
171
+ c := &Client{
172
+ apiKey: apiKey,
173
+ baseURL: "https://api.workos.com",
174
+ httpClient: &http.Client{Timeout: 60 * time.Second},
175
+ maxRetries: 3,
176
+ }
177
+ for _, opt := range opts {
178
+ opt(c)
179
+ }
180
+ c.organizations = &organizationService{client: c}
181
+ c.users = &userService{client: c}
182
+ return c
183
+ }
184
+
185
+ // Organizations returns the organizations service.
186
+ func (c *Client) Organizations() *organizationService {
187
+ return c.organizations
188
+ }
189
+ ```
190
+
191
+ ### Functional Options
192
+
193
+ ```go
194
+ type ClientOption func(*Client)
195
+
196
+ func WithBaseURL(url string) ClientOption {
197
+ return func(c *Client) { c.baseURL = url }
198
+ }
199
+
200
+ func WithHTTPClient(client *http.Client) ClientOption {
201
+ return func(c *Client) { c.httpClient = client }
202
+ }
203
+
204
+ func WithMaxRetries(n int) ClientOption {
205
+ return func(c *Client) { c.maxRetries = n }
206
+ }
207
+ ```
208
+
209
+ ### Per-Request Options
210
+
211
+ ```go
212
+ type RequestOption func(*requestConfig)
213
+
214
+ type requestConfig struct {
215
+ extraHeaders http.Header
216
+ timeout time.Duration
217
+ maxRetries *int
218
+ baseURL string
219
+ idempotencyKey string
220
+ }
221
+
222
+ func WithExtraHeaders(h http.Header) RequestOption {
223
+ return func(r *requestConfig) { r.extraHeaders = h }
224
+ }
225
+
226
+ func WithTimeout(d time.Duration) RequestOption {
227
+ return func(r *requestConfig) { r.timeout = d }
228
+ }
229
+
230
+ func WithIdempotencyKey(key string) RequestOption {
231
+ return func(r *requestConfig) { r.idempotencyKey = key }
232
+ }
233
+ ```
234
+
235
+ ## Pagination Pattern
236
+
237
+ ```go
238
+ // Iterator provides auto-pagination over list endpoints.
239
+ type Iterator[T any] struct {
240
+ cur *T
241
+ items []T
242
+ err error
243
+ params listParams
244
+ fetcher func(context.Context, listParams) (*listResponse[T], error)
245
+ ctx context.Context
246
+ done bool
247
+ }
248
+
249
+ // Next advances the iterator. Returns false when done or on error.
250
+ func (it *Iterator[T]) Next() bool { ... }
251
+
252
+ // Current returns the current item.
253
+ func (it *Iterator[T]) Current() *T { return it.cur }
254
+
255
+ // Err returns any error from the last page fetch.
256
+ func (it *Iterator[T]) Err() error { return it.err }
257
+ ```
258
+
259
+ ## Error Handling
260
+
261
+ ```go
262
+ type APIError struct {
263
+ StatusCode int `json:"-"`
264
+ RequestID string `json:"-"`
265
+ Code string `json:"code"`
266
+ Message string `json:"message"`
267
+ }
268
+
269
+ func (e *APIError) Error() string {
270
+ return fmt.Sprintf("workos: %d %s: %s", e.StatusCode, e.Code, e.Message)
271
+ }
272
+
273
+ // Sentinel types for errors.Is() / type assertions
274
+ type AuthenticationError struct{ *APIError }
275
+ type NotFoundError struct{ *APIError }
276
+ type RateLimitExceededError struct{ *APIError }
277
+ type UnprocessableEntityError struct{ *APIError }
278
+ type ServerError struct{ *APIError }
279
+ ```
280
+
281
+ ## Retry Logic
282
+
283
+ - Exponential backoff with jitter: `min(base * 2^attempt + jitter, maxDelay)`
284
+ - Retryable statuses: 429, 500, 502, 503, 504
285
+ - Respect `Retry-After` header
286
+ - Auto-generate UUID idempotency key for POST requests, reused across retries
287
+ - Default max retries: 3
288
+
289
+ ## Test Pattern
290
+
291
+ ```go
292
+ func TestOrganizations_GetOrganization(t *testing.T) {
293
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
294
+ require.Equal(t, http.MethodGet, r.Method)
295
+ require.Equal(t, "/organizations/org_123", r.URL.Path)
296
+ w.Header().Set("Content-Type", "application/json")
297
+ w.WriteHeader(http.StatusOK)
298
+ fixture, _ := os.ReadFile("testdata/organization.json")
299
+ w.Write(fixture)
300
+ }))
301
+ defer server.Close()
302
+
303
+ client := workos.NewClient("sk_test", workos.WithBaseURL(server.URL))
304
+ org, err := client.Organizations().GetOrganization(context.Background(), "org_123")
305
+ require.NoError(t, err)
306
+ require.Equal(t, "org_123", org.ID)
307
+ }
308
+ ```
309
+
310
+ ## Structural Guidelines
311
+
312
+ | Category | Choice |
313
+ | ------------------ | ------------------------------------------------- |
314
+ | Testing Framework | `testing` + `github.com/stretchr/testify/require` |
315
+ | HTTP Mocking | `net/http/httptest` |
316
+ | Documentation | GoDoc comments |
317
+ | Type Signatures | Native Go types (inline) |
318
+ | Linting/Formatting | `gofmt` / `go vet` |
319
+ | HTTP Client | `net/http` (stdlib) |
320
+ | JSON Parsing | `encoding/json` (stdlib) |
321
+ | Package Manager | Go modules (`go.mod`) |
322
+ | Build Tool | `go build` / `go test` |
323
+
324
+ ## Directory Structure (generated SDK)
325
+
326
+ ```
327
+ workos-go/
328
+ +-- go.mod
329
+ +-- go.sum
330
+ +-- workos.go // Package doc, NewClient, ClientOption, RequestOption
331
+ +-- client.go // Client struct, HTTP request execution, retry logic
332
+ +-- errors.go // Error types
333
+ +-- pagination.go // Iterator[T]
334
+ +-- {service}.go // Models, enums, params, service client for each service
335
+ +-- {service}_test.go // Tests for each service
336
+ +-- testdata/
337
+ | +-- {model}.json // JSON fixtures
338
+ ```
@@ -0,0 +1,315 @@
1
+ # PHP SDK Architecture
2
+
3
+ ## Overview
4
+
5
+ A PHP 8.2+ SDK using readonly classes, backed enums, Guzzle for HTTP, and PHPUnit for testing. PSR-4 autoloading with Composer.
6
+
7
+ ## Naming Conventions
8
+
9
+ | IR Name | PHP Convention | Example |
10
+ | --------------------- | -------------- | ----------------- |
11
+ | `UserProfile` (class) | PascalCase | `UserProfile` |
12
+ | `UserProfile` (file) | PascalCase.php | `UserProfile.php` |
13
+ | `listUsers` (method) | camelCase | `listUsers` |
14
+ | `user_id` (field) | camelCase | `userId` |
15
+ | `ACTIVE` (enum case) | PascalCase | `Active` |
16
+
17
+ ## Type Mapping
18
+
19
+ | IR TypeRef | PHP Type Hint | PHPDoc Type |
20
+ | -------------------- | -------------------- | -------------------- |
21
+ | `string` | `string` | `string` |
22
+ | `string` (date) | `string` | `string` |
23
+ | `string` (date-time) | `\DateTimeImmutable` | `\DateTimeImmutable` |
24
+ | `string` (uuid) | `string` | `string` |
25
+ | `string` (binary) | `string` | `string` |
26
+ | `integer` | `int` | `int` |
27
+ | `number` | `float` | `float` |
28
+ | `boolean` | `bool` | `bool` |
29
+ | `unknown` | `mixed` | `mixed` |
30
+ | `array<T>` | `array` | `array<T>` |
31
+ | `model Foo` | `Foo` | `Foo` |
32
+ | `enum Foo` | `Foo` | `Foo` |
33
+ | `nullable<T>` | `?T` | `T\|null` |
34
+ | `union<A,B>` | `A\|B` | `A\|B` |
35
+ | `map<string,V>` | `array` | `array<string, V>` |
36
+ | `literal "foo"` | `string` | `string` |
37
+
38
+ ## Model Pattern
39
+
40
+ Readonly classes with constructor promotion, `fromArray()` factory, and `toArray()` serialization:
41
+
42
+ ```php
43
+ <?php
44
+
45
+ namespace WorkOS\Models;
46
+
47
+ readonly class Organization implements \JsonSerializable
48
+ {
49
+ public function __construct(
50
+ public string $id,
51
+ public string $name,
52
+ public ?string $slug = null,
53
+ public ?\DateTimeImmutable $createdAt = null,
54
+ ) {}
55
+
56
+ public static function fromArray(array $data): static
57
+ {
58
+ return new static(
59
+ id: $data['id'],
60
+ name: $data['name'],
61
+ slug: $data['slug'] ?? null,
62
+ createdAt: isset($data['created_at'])
63
+ ? new \DateTimeImmutable($data['created_at'])
64
+ : null,
65
+ );
66
+ }
67
+
68
+ public function toArray(): array
69
+ {
70
+ return array_filter([
71
+ 'id' => $this->id,
72
+ 'name' => $this->name,
73
+ 'slug' => $this->slug,
74
+ 'created_at' => $this->createdAt?->format(\DateTimeInterface::RFC3339_EXTENDED),
75
+ ], fn ($v) => $v !== null);
76
+ }
77
+
78
+ public function jsonSerialize(): array
79
+ {
80
+ return $this->toArray();
81
+ }
82
+ }
83
+ ```
84
+
85
+ ## Enum Pattern
86
+
87
+ PHP 8.1+ backed enums:
88
+
89
+ ```php
90
+ <?php
91
+
92
+ namespace WorkOS\Enums;
93
+
94
+ enum OrganizationStatus: string
95
+ {
96
+ case Active = 'active';
97
+ case Inactive = 'inactive';
98
+
99
+ public static function tryFromValue(string $value): self|string
100
+ {
101
+ return self::tryFrom($value) ?? $value;
102
+ }
103
+ }
104
+ ```
105
+
106
+ ## Resource Pattern
107
+
108
+ Resource classes with typed methods, injected HTTP client:
109
+
110
+ ```php
111
+ <?php
112
+
113
+ namespace WorkOS\Resources;
114
+
115
+ use WorkOS\HttpClient;
116
+ use WorkOS\Models\Organization;
117
+ use WorkOS\PaginatedResponse;
118
+ use WorkOS\RequestOptions;
119
+
120
+ class Organizations
121
+ {
122
+ public function __construct(
123
+ private readonly HttpClient $client,
124
+ ) {}
125
+
126
+ public function get(string $id, ?RequestOptions $options = null): Organization
127
+ {
128
+ $response = $this->client->request(
129
+ method: 'GET',
130
+ path: "organizations/{$id}",
131
+ options: $options,
132
+ );
133
+ return Organization::fromArray($response);
134
+ }
135
+
136
+ public function list(
137
+ ?int $limit = null,
138
+ ?string $after = null,
139
+ ?RequestOptions $options = null,
140
+ ): PaginatedResponse
141
+ {
142
+ $response = $this->client->request(
143
+ method: 'GET',
144
+ path: 'organizations',
145
+ query: array_filter([
146
+ 'limit' => $limit,
147
+ 'after' => $after,
148
+ ], fn ($v) => $v !== null),
149
+ options: $options,
150
+ );
151
+ return PaginatedResponse::fromArray($response, Organization::class);
152
+ }
153
+ }
154
+ ```
155
+
156
+ ## Client Architecture
157
+
158
+ Main client with resource accessors:
159
+
160
+ ```php
161
+ <?php
162
+
163
+ namespace WorkOS;
164
+
165
+ class WorkOS
166
+ {
167
+ private HttpClient $httpClient;
168
+ private ?Resources\Organizations $organizations = null;
169
+
170
+ public function __construct(
171
+ string $apiKey = null,
172
+ string $baseUrl = 'https://api.workos.com',
173
+ int $timeout = 60,
174
+ int $maxRetries = 3,
175
+ ) {
176
+ $apiKey ??= getenv('WORKOS_API_KEY') ?: '';
177
+ $this->httpClient = new HttpClient($apiKey, $baseUrl, $timeout, $maxRetries);
178
+ }
179
+
180
+ public function organizations(): Resources\Organizations
181
+ {
182
+ return $this->organizations ??= new Resources\Organizations($this->httpClient);
183
+ }
184
+ }
185
+ ```
186
+
187
+ ## Error Handling
188
+
189
+ Exception hierarchy extending a base `ApiException`:
190
+
191
+ ```php
192
+ <?php
193
+
194
+ namespace WorkOS\Exceptions;
195
+
196
+ class ApiException extends \Exception { /* status_code, request_id, etc. */ }
197
+ class AuthenticationException extends ApiException {}
198
+ class BadRequestException extends ApiException {}
199
+ class NotFoundException extends ApiException {}
200
+ class UnprocessableEntityException extends ApiException {}
201
+ class RateLimitExceededException extends ApiException {}
202
+ class ServerException extends ApiException {}
203
+ class ConfigurationException extends \Exception {}
204
+ class ConnectionException extends \Exception {}
205
+ class TimeoutException extends \Exception {}
206
+ ```
207
+
208
+ ## Pagination
209
+
210
+ Generic paginated response with auto-iteration:
211
+
212
+ ```php
213
+ <?php
214
+
215
+ namespace WorkOS;
216
+
217
+ class PaginatedResponse implements \IteratorAggregate
218
+ {
219
+ public function __construct(
220
+ public readonly array $data,
221
+ public readonly array $listMetadata,
222
+ private readonly ?\Closure $fetchPage = null,
223
+ ) {}
224
+
225
+ public function hasMore(): bool { return ($this->listMetadata['after'] ?? null) !== null; }
226
+
227
+ public function getIterator(): \Generator
228
+ {
229
+ $page = $this;
230
+ while (true) {
231
+ yield from $page->data;
232
+ if (!$page->hasMore() || $page->fetchPage === null) break;
233
+ $page = ($page->fetchPage)(['after' => $page->listMetadata['after']]);
234
+ }
235
+ }
236
+ }
237
+ ```
238
+
239
+ ## Retry Logic
240
+
241
+ Exponential backoff with jitter. Retryable statuses: 429, 500, 502, 503, 504. Respects `Retry-After` header.
242
+
243
+ ## Testing
244
+
245
+ PHPUnit with Guzzle `MockHandler`:
246
+
247
+ ```php
248
+ <?php
249
+
250
+ namespace Tests\Resources;
251
+
252
+ use GuzzleHttp\Handler\MockHandler;
253
+ use GuzzleHttp\HandlerStack;
254
+ use GuzzleHttp\Psr7\Response;
255
+ use PHPUnit\Framework\TestCase;
256
+ use WorkOS\WorkOS;
257
+
258
+ class OrganizationsTest extends TestCase
259
+ {
260
+ public function testGet(): void
261
+ {
262
+ $fixture = json_decode(file_get_contents(__DIR__ . '/../Fixtures/organization.json'), true);
263
+ $mock = new MockHandler([new Response(200, [], json_encode($fixture))]);
264
+ $client = new WorkOS(apiKey: 'test', handler: HandlerStack::create($mock));
265
+
266
+ $result = $client->organizations()->get('org_01234');
267
+ $this->assertInstanceOf(\WorkOS\Models\Organization::class, $result);
268
+ }
269
+ }
270
+ ```
271
+
272
+ ## Structural Guidelines
273
+
274
+ | Category | Choice |
275
+ | ------------------ | -------------------------- |
276
+ | PHP Version | 8.2+ |
277
+ | HTTP Client | Guzzle 7 |
278
+ | Testing | PHPUnit 11 |
279
+ | HTTP Mocking | Guzzle MockHandler |
280
+ | Documentation | PHPDoc |
281
+ | Type Signatures | Native type hints + PHPDoc |
282
+ | Linting/Formatting | PHP CS Fixer |
283
+ | JSON Parsing | Native json_decode/encode |
284
+ | Package Manager | Composer |
285
+ | Build Tool | N/A (interpreted) |
286
+
287
+ ## Directory Structure
288
+
289
+ ```
290
+ src/
291
+ ├── {Namespace}.php # Main client class
292
+ ├── HttpClient.php # HTTP client with retry logic
293
+ ├── PaginatedResponse.php # Cursor pagination
294
+ ├── RequestOptions.php # Per-request options
295
+ ├── Enums/
296
+ │ └── {EnumName}.php
297
+ ├── Models/
298
+ │ └── {ModelName}.php
299
+ ├── Resources/
300
+ │ └── {ServiceName}.php
301
+ └── Exceptions/
302
+ ├── ApiException.php
303
+ ├── AuthenticationException.php
304
+ └── ...
305
+ tests/
306
+ ├── Fixtures/
307
+ │ └── {model_name}.json
308
+ ├── Resources/
309
+ │ └── {ServiceName}Test.php
310
+ ├── Models/
311
+ │ └── {ModelName}Test.php
312
+ └── ClientTest.php
313
+ composer.json
314
+ phpunit.xml
315
+ ```