Skip to content

feat(organizations): add Organizations resource, Page<T>, and list-options pattern#16

Merged
fbarrento merged 1 commit into
mainfrom
feat/3-organizations
May 15, 2026
Merged

feat(organizations): add Organizations resource, Page<T>, and list-options pattern#16
fbarrento merged 1 commit into
mainfrom
feat/3-organizations

Conversation

@fbarrento
Copy link
Copy Markdown
Contributor

Closes #3.

Summary

First paginated resource. Establishes the Page<T> + List{Resource}Options + Saloon BaseResource patterns that every subsequent resource (#4#9) will reuse.

Generic infrastructure

  • Data\Page<T> — generic readonly page with data, nextCursor, prevCursor, size. Implements IteratorAggregate (yields items) and JsonSerializable. hasMore() reflects nextCursor !== null.
  • Data\Organization — readonly DTO, hydrated from a JSON:API resource object via Organization::from(). Throws on missing required keys; dates parsed to DateTimeImmutable. Implements JsonSerializable.
  • Data\ListOrganizationsOptions — readonly DTO with size + cursor. toQuery() emits Forge's page[size] / page[cursor] shape. Spec doesn't define sort or filter[] on /orgs, so the DTO matches.

Saloon layer

  • Resources\OrganizationsResource (extends Saloon\Http\BaseResource) — all(?Options): Page<Organization> for one HTTP call; iterate(?Options): Generator<Organization> for lazy auto-pagination following meta.next_cursor until null.
  • Resources\OrganizationResourceget(): Organization for a single org by slug.
  • Requests\Organizations\GetOrganizations (GET /orgs) and GetOrganization (GET /orgs/{slug}).

Forge facade

  • $forge->organizations()OrganizationsResource
  • $forge->organization($slug)OrganizationResource
  • The default-org constructor property is renamed defaultOrganization to free the organization() method name. README + Forge tests updated.

Tests (ortto-sdk style)

One test file per source class, mirroring src/, all under tests/Unit/:

  • Data/{Page,Organization,ListOrganizationsOptions}Test.php
  • Requests/Organizations/{GetOrganization,GetOrganizations}Test.php
  • Resources/{Organization,Organizations}ResourceTest.php
  • The 35-test ForgeTest.php from the previous PR is split into smaller per-source-class files:
    • tests/Unit/ForgeTest.php (constructor + factories + accessor types, ~16 tests)
    • tests/Unit/Requests/Me/GetMeTest.php (5 happy-path + 13 error-mapping)
    • tests/Unit/Exceptions/ValidationExceptionTest.php (3 tests)
    • tests/Unit/Exceptions/RateLimitExceptionTest.php (2 tests)

Fixtures

Recorded against the real Forge API (no hand-crafted bodies), committed under tests/Fixtures/Saloon/organizations/{list,get}.json. ForgeFixture now redacts org slugs in both attributes.slug (JSON-key pass) and links.self.href URLs (regex pass that handles both / and JSON-escaped \/), mapping consistently so attributes.slug=test-org-1 always aligns with .../orgs/test-org-1.

Notes

  • The iterate() pagination control-flow test uses a sequence of MockResponse::make() (the only deterministic way to test cursor-following without making the test depend on the user's exact org count). All shape assertions still go through the real recorded fixture.

Test plan

  • composer test green locally (Pint, Rector, PHPStan max, Pest at --exactly=100, 76/76 tests)
  • CI matrix green across P{8.4,8.5} × {ubuntu, macos, windows} × {prefer-lowest, prefer-stable}
  • Confirm fixtures contain no real slugs anywhere (already verified locally — slugs replaced in both attribute and URL contexts)

🤖 Generated with Claude Code

Closes #3. Establishes the resource/DTO/pagination shape that #4#9 will reuse.

Code:
- `Data\Page<T>` — generic readonly Page with `data`, `nextCursor`, `prevCursor`,
  `size`. Implements `IteratorAggregate` (yields items) and `JsonSerializable`.
- `Data\Organization` — readonly DTO, JSON:API hydration via `::from()` with
  unknown-key/missing-key throws, dates parsed to `DateTimeImmutable`.
- `Data\ListOrganizationsOptions` — readonly DTO, `toQuery()` emits Forge's
  `page[size]` / `page[cursor]` shape.
- `Resources\OrganizationsResource` — extends `Saloon\Http\BaseResource`,
  exposes `all()` returning `Page<Organization>` and `iterate()` returning a
  `Generator<Organization>` that follows `meta.next_cursor` until null.
- `Resources\OrganizationResource` — `get()` returning a hydrated Organization.
- `Requests\Organizations\GetOrganizations` (GET /orgs) and `GetOrganization`
  (GET /orgs/{slug}).
- `Forge::organizations()` and `Forge::organization($slug)` accessors. The
  default-org property is renamed `defaultOrganization` to avoid collision
  with the new `organization($slug)` method.

Tests (one file per source class, mirroring `src/`, ortto-sdk style):
- Per-DTO `tests/Unit/Data/{Page,Organization,ListOrganizationsOptions}Test.php`
- Per-Request `tests/Unit/Requests/Organizations/{GetOrganization,GetOrganizations}Test.php`
- Per-Resource `tests/Unit/Resources/{Organization,Organizations}ResourceTest.php`
- Forge facade tests refactored to use `defaultOrganization` and assert the
  resource accessor types.
- Existing per-Exception tests stay; the giant ForgeTest split into smaller
  per-source-class files (`Requests/Me/GetMeTest`, `Exceptions/{Validation,RateLimit}ExceptionTest`).

Fixtures recorded against the real Forge API and committed under
`tests/Fixtures/Saloon/organizations/`. `ForgeFixture` now redacts org slugs
in both `attributes.slug` (JSON field pass) and `links.self.href` (regex pass
that handles both raw and JSON-escaped slashes), mapping consistently so
e.g. `attributes.slug=test-org-1` always lines up with `.../orgs/test-org-1`.

76/76 tests pass at 100% line + type coverage, Pint, Rector, PHPStan max clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@fbarrento fbarrento self-assigned this May 15, 2026
@fbarrento fbarrento changed the title Organizations resource + Page<T> + list-options pattern feat: organizations resource + Page<T> + list-options pattern May 15, 2026
@fbarrento fbarrento merged commit 5f243c7 into main May 15, 2026
26 checks passed
@fbarrento fbarrento changed the title feat: organizations resource + Page<T> + list-options pattern feat(organizations): add Organizations resource, Page<T>, and list-options pattern May 15, 2026
fbarrento added a commit that referenced this pull request May 19, 2026
Captures the four spec mismatches surfaced while building the Servers
slice (#18 + #19) plus the open-strings-not-enums decision from the
Providers slice (#16 + #17). Append future findings here so the next
debugging session can find the answer without re-running live-fire
investigations.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@fbarrento fbarrento mentioned this pull request May 24, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Organizations resource + Page<T> + list-options pattern

1 participant