Add TeamAPI and resource classes for team-level management#10
Conversation
- Add auth_scheme parameter to HttpClient for project/team token auth - Extract SendingAPI from Client for project-level email sending - Add TeamAPI for team-level management operations with Bearer auth - Add Resources::Base with shared pagination/filter helpers - Add Resources::Team for /team endpoints - Add Resources::Domains with full CRUD and DNS verification - Add stub files for remaining resources (projects, routes, etc.) - Simplify Client to alias SendingAPI for backward compatibility - Update require chain in lettermint.rb
- SendingAPI specs: token validation, x-lettermint-token auth, HTTP methods, configuration, error propagation, Client alias backward compat - TeamAPI specs: lm_team_* token format validation, Bearer auth header, resource accessor methods - Resources::Base specs: build_params helper for pagination, sort, filters - Resources::Team specs: get, update, usage, members endpoints - Resources::Domains specs: full CRUD, DNS verification, project association 253 examples, 0 failures
There was a problem hiding this comment.
Code Review
This pull request significantly expands the Lettermint Ruby SDK by introducing a comprehensive suite of resource classes to support the Team API, including Domains, Messages, Projects, Routes, Stats, Suppressions, Team, and Webhooks. The architecture is refactored to distinguish between SendingAPI and TeamAPI, accommodating different authentication schemes while maintaining backward compatibility for the existing client. Review feedback highlights the need for explicit Accept headers on non-JSON endpoints in the Messages resource, suggests more robust handling of authentication symbols, and recommends refactoring the parameter-building logic to be more idiomatic.
| # Get raw message source (RFC822 format). | ||
| # @param id [String] Message ID | ||
| # @return [String] Raw message source (message/rfc822) | ||
| def source(id) |
There was a problem hiding this comment.
The HttpClient defaults to Accept: application/json. Since this endpoint returns raw RFC822 message source, you should explicitly set the Accept header to message/rfc822 to ensure the server provides the correct content type and the client handles it as a raw string.
def source(id)
@http_client.get(path: "/messages/#{id}/source", headers: { 'Accept' => 'message/rfc822' })
end| # Get message HTML body. | ||
| # @param id [String] Message ID | ||
| # @return [String] HTML content (text/html) | ||
| def html(id) |
| # Get message plain text body. | ||
| # @param id [String] Message ID | ||
| # @return [String] Plain text content (text/plain) | ||
| def text(id) |
There was a problem hiding this comment.
| def auth_headers(token, scheme) | ||
| case scheme | ||
| when :project then { 'x-lettermint-token' => token } | ||
| when :team then { 'Authorization' => "Bearer #{token}" } | ||
| else raise ArgumentError, "Unknown auth_scheme: #{scheme}" | ||
| end | ||
| end |
There was a problem hiding this comment.
The scheme parameter is compared directly in the case statement. To make the client more robust against callers passing strings instead of symbols, it is safer to convert the scheme to a symbol before comparison.
def auth_headers(token, scheme)
case scheme.to_sym
when :project then { 'x-lettermint-token' => token }
when :team then { 'Authorization' => "Bearer #{token}" }
else raise ArgumentError, "Unknown auth_scheme: #{scheme}"
end
endThere was a problem hiding this comment.
Pull request overview
This PR introduces a split-client SDK architecture by extracting the existing project-scoped sending client into Lettermint::SendingAPI and adding a new Lettermint::TeamAPI for team-scoped management endpoints, with Lettermint::Client retained as a backward-compatible alias. It also adds a set of TeamAPI resource classes with shared pagination/filter helpers and updates HttpClient to support configurable authentication schemes.
Changes:
- Add
SendingAPI(project token viax-lettermint-token) andTeamAPI(team token viaAuthorization: Bearer), withClientaliased toSendingAPI. - Add
Resources::Baseand multiple TeamAPI resource classes (Team, Domains, Projects, Routes, Webhooks, Messages, Suppressions, Stats). - Extend
HttpClientwithauth_schemeto switch header behavior, and add RSpec coverage for the new clients + some resources.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/lettermint/team_api_spec.rb | Adds TeamAPI initialization/auth header/accessor/error specs |
| spec/lettermint/sending_api_spec.rb | Adds SendingAPI initialization/auth header/http delegation + Client alias spec |
| spec/lettermint/resources/team_spec.rb | Adds Team resource endpoint specs |
| spec/lettermint/resources/domains_spec.rb | Adds Domains resource CRUD + verification/association specs |
| spec/lettermint/resources/base_spec.rb | Adds specs for shared pagination/sort/filter param builder |
| lib/lettermint/team_api.rb | Introduces TeamAPI client and resource accessors |
| lib/lettermint/sending_api.rb | Introduces SendingAPI client extracted from prior Client |
| lib/lettermint/resources/base.rb | Adds shared query param building helper for resources |
| lib/lettermint/resources/team.rb | Adds Team endpoints (get/update/usage/members) |
| lib/lettermint/resources/domains.rb | Adds Domains endpoints (list/create/find/delete/verify/update_projects) |
| lib/lettermint/resources/projects.rb | Adds Projects endpoints + nested routes accessor |
| lib/lettermint/resources/routes.rb | Adds Routes endpoints (scoped list/create + direct CRUD/verify) |
| lib/lettermint/resources/webhooks.rb | Adds Webhooks endpoints + deliveries access |
| lib/lettermint/resources/messages.rb | Adds Messages endpoints + sub-resources (events/source/html/text) |
| lib/lettermint/resources/suppressions.rb | Adds Suppressions list/create/delete |
| lib/lettermint/resources/stats.rb | Adds Stats endpoint wrapper |
| lib/lettermint/http_client.rb | Adds auth_scheme + auth header selection helper |
| lib/lettermint/client.rb | Replaces old Client implementation with alias to SendingAPI |
| lib/lettermint.rb | Updates require chain to load new clients/resources |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Get project details. | ||
| # @param id [String] Project ID | ||
| # @param include [String, nil] Related data: routes, domains, teamMembers, messageStats (+ Count/Exists variants) | ||
| # @return [Hash] Project data with optional includes | ||
| def find(id, include: nil) | ||
| params = include ? { 'include' => include } : nil | ||
| @http_client.get(path: "/projects/#{id}", params: params) | ||
| end |
There was a problem hiding this comment.
PR description shows chaining team.projects.find('uuid').routes.list, but Projects#find returns a parsed Hash, so that call chain will fail. If the intended fluent API is projects.find(id).routes, consider returning a resource object from find (or adding a project(id) wrapper that exposes nested resources); otherwise update docs/examples to use team.projects.routes('uuid').list.
| # @return [Hash] Stats data with totals and daily breakdown | ||
| def get(from:, to:, project_id: nil) | ||
| params = { 'from' => from, 'to' => to } | ||
| params['project_id'] = project_id if project_id |
There was a problem hiding this comment.
Resources::Stats#get only supports project_id, but the PR description / Issue #9 call out stats filters for date + project + route. If the API supports route-level stats, consider adding an optional route_id (and/or route_ids) parameter and including it in the query params so consumers can retrieve route-scoped stats as advertised.
| # @return [Hash] Stats data with totals and daily breakdown | |
| def get(from:, to:, project_id: nil) | |
| params = { 'from' => from, 'to' => to } | |
| params['project_id'] = project_id if project_id | |
| # @param route_id [String, nil] Filter by a single route ID | |
| # @param route_ids [Array<String>, String, nil] Filter by multiple route IDs | |
| # @return [Hash] Stats data with totals and daily breakdown | |
| def get(from:, to:, project_id: nil, route_id: nil, route_ids: nil) | |
| params = { 'from' => from, 'to' => to } | |
| params['project_id'] = project_id if project_id | |
| params['route_id'] = route_id if route_id | |
| params['route_ids'] = route_ids if route_ids |
| class Webhooks < Base | ||
| # List all webhooks. | ||
| # @param page_size [Integer, nil] Number of items per page (default: 30) | ||
| # @param page_cursor [String, nil] Cursor for pagination | ||
| # @param sort [String, nil] Sort field: name, url, created_at (prefix - for desc) | ||
| # @param enabled [Boolean, nil] Filter by enabled status | ||
| # @param event [String, nil] Filter by event type | ||
| # @param route_id [String, nil] Filter by route ID | ||
| # @param search [String, nil] Search filter | ||
| # @return [Hash] Paginated list of webhooks | ||
| # rubocop:disable Metrics/ParameterLists | ||
| def list(page_size: nil, page_cursor: nil, sort: nil, enabled: nil, | ||
| event: nil, route_id: nil, search: nil) | ||
| params = build_params( | ||
| page_size: page_size, | ||
| page_cursor: page_cursor, | ||
| sort: sort, | ||
| enabled: enabled, | ||
| event: event, | ||
| route_id: route_id, | ||
| search: search | ||
| ) | ||
| @http_client.get(path: '/webhooks', params: params) | ||
| end |
There was a problem hiding this comment.
This resource introduces multiple endpoints (list/create/find/update/delete/test/regenerate_secret/deliveries) but there are no dedicated specs exercising these methods (only an accessor check in team_api_spec). Add a spec/lettermint/resources/webhooks_spec.rb similar to domains_spec.rb/team_spec.rb to verify paths, query params, and error handling.
| class Messages < Base | ||
| # List messages. | ||
| # @param page_size [Integer, nil] Number of items per page (default: 30) | ||
| # @param page_cursor [String, nil] Cursor for pagination | ||
| # @param sort [String, nil] Sort field: type, status, from_email, subject, created_at, status_changed_at | ||
| # @param type [String, nil] Filter: inbound, outbound | ||
| # @param status [String, nil] Filter by status | ||
| # @param route_id [String, nil] Filter by route ID | ||
| # @param domain_id [String, nil] Filter by domain ID | ||
| # @param tag [String, nil] Filter by tag | ||
| # @param from_email [String, nil] Filter by sender email | ||
| # @param subject [String, nil] Filter by subject | ||
| # @param from_date [String, nil] Filter from date (Y-m-d) | ||
| # @param to_date [String, nil] Filter to date (Y-m-d) | ||
| # @return [Hash] Paginated list of messages | ||
| # rubocop:disable Metrics/ParameterLists | ||
| def list(page_size: nil, page_cursor: nil, sort: nil, type: nil, status: nil, | ||
| route_id: nil, domain_id: nil, tag: nil, from_email: nil, subject: nil, | ||
| from_date: nil, to_date: nil) | ||
| params = build_params( | ||
| page_size: page_size, | ||
| page_cursor: page_cursor, | ||
| sort: sort, | ||
| type: type, | ||
| status: status, | ||
| route_id: route_id, | ||
| domain_id: domain_id, | ||
| tag: tag, | ||
| from_email: from_email, | ||
| subject: subject, | ||
| from_date: from_date, | ||
| to_date: to_date | ||
| ) | ||
| @http_client.get(path: '/messages', params: params) | ||
| end |
There was a problem hiding this comment.
This resource adds several endpoints (list, find, events, source, html, text) but there are no specs covering the request paths/params or the expected response parsing. Add a spec/lettermint/resources/messages_spec.rb to validate each endpoint and content-type handling (JSON vs plain text).
| class Projects < Base | ||
| # List all projects. | ||
| # @param page_size [Integer, nil] Number of items per page (default: 30) | ||
| # @param page_cursor [String, nil] Cursor for pagination | ||
| # @param sort [String, nil] Sort field: name, created_at (prefix - for desc) | ||
| # @param search [String, nil] Search filter | ||
| # @return [Hash] Paginated list of projects | ||
| def list(page_size: nil, page_cursor: nil, sort: nil, search: nil) | ||
| params = build_params(page_size: page_size, page_cursor: page_cursor, sort: sort, search: search) | ||
| @http_client.get(path: '/projects', params: params) | ||
| end | ||
|
|
||
| # Create a new project. | ||
| # @param name [String] Project name (max 255 chars) | ||
| # @param smtp_enabled [Boolean, nil] Enable SMTP (default: false) | ||
| # @param initial_routes [String, nil] Initial routes: both, transactional, broadcast (default: both) | ||
| # @return [Hash] Created project data including api_token | ||
| def create(name:, smtp_enabled: nil, initial_routes: nil) | ||
| data = { name: name } | ||
| data[:smtp_enabled] = smtp_enabled unless smtp_enabled.nil? | ||
| data[:initial_routes] = initial_routes if initial_routes | ||
| @http_client.post(path: '/projects', data: data) | ||
| end |
There was a problem hiding this comment.
Resources::Projects adds CRUD, token rotation, member management, and a nested routes(project_id) accessor, but there are no resource-level specs validating the HTTP calls. Add a spec/lettermint/resources/projects_spec.rb to cover each method (paths, request bodies, and error propagation).
| # List routes for a project. | ||
| # Requires project_id to be set (via constructor or projects.routes(id)). | ||
| # @param page_size [Integer, nil] Number of items per page (default: 30) | ||
| # @param page_cursor [String, nil] Cursor for pagination | ||
| # @param sort [String, nil] Sort field: name, slug, created_at (prefix - for desc) | ||
| # @param route_type [String, nil] Filter: transactional, broadcast, inbound | ||
| # @param is_default [Boolean, nil] Filter by default route status | ||
| # @param search [String, nil] Search filter | ||
| # @return [Hash] Paginated list of routes | ||
| # rubocop:disable Metrics/ParameterLists | ||
| def list(page_size: nil, page_cursor: nil, sort: nil, route_type: nil, | ||
| is_default: nil, search: nil) | ||
| raise ArgumentError, 'project_id required for listing routes' unless @project_id | ||
|
|
||
| params = build_params( | ||
| page_size: page_size, | ||
| page_cursor: page_cursor, | ||
| sort: sort, | ||
| route_type: route_type, | ||
| is_default: is_default, | ||
| search: search | ||
| ) | ||
| @http_client.get(path: "/projects/#{@project_id}/routes", params: params) | ||
| end |
There was a problem hiding this comment.
Resources::Routes includes project-scoped methods (list, create) plus direct route methods (find, update, delete, verify_inbound_domain), but there are no specs confirming the correct endpoints and the project_id required guard behavior. Add a spec/lettermint/resources/routes_spec.rb to cover both scoped and unscoped usage.
| class Suppressions < Base | ||
| # List suppressions. | ||
| # @param page_size [Integer, nil] Number of items per page (default: 30) | ||
| # @param page_cursor [String, nil] Cursor for pagination | ||
| # @param sort [String, nil] Sort field: value, created_at, reason | ||
| # @param scope [String, nil] Filter: team, project, route | ||
| # @param route_id [String, nil] Filter by route ID | ||
| # @param project_id [String, nil] Filter by project ID | ||
| # @param value [String, nil] Filter by suppression value (email/domain/extension) | ||
| # @param reason [String, nil] Filter: spam_complaint, hard_bounce, unsubscribe, manual | ||
| # @return [Hash] Paginated list of suppressions | ||
| # rubocop:disable Metrics/ParameterLists | ||
| def list(page_size: nil, page_cursor: nil, sort: nil, scope: nil, | ||
| route_id: nil, project_id: nil, value: nil, reason: nil) | ||
| params = build_params( | ||
| page_size: page_size, | ||
| page_cursor: page_cursor, | ||
| sort: sort, | ||
| scope: scope, | ||
| route_id: route_id, | ||
| project_id: project_id, | ||
| value: value, | ||
| reason: reason | ||
| ) | ||
| @http_client.get(path: '/suppressions', params: params) | ||
| end |
There was a problem hiding this comment.
Resources::Suppressions adds list/create/delete behavior with several query/body options, but there are no specs validating parameter mapping and request bodies. Add a spec/lettermint/resources/suppressions_spec.rb similar to domains_spec.rb to ensure filters/pagination and creation payloads are encoded correctly.
| # Stats resource for retrieving email statistics. | ||
| class Stats < Base | ||
| # Get statistics for a date range. | ||
| # @param from [String] Start date (Y-m-d format, required) | ||
| # @param to [String] End date (Y-m-d format, required, max 90 days from start) | ||
| # @param project_id [String, nil] Filter by project ID | ||
| # @return [Hash] Stats data with totals and daily breakdown | ||
| def get(from:, to:, project_id: nil) | ||
| params = { 'from' => from, 'to' => to } | ||
| params['project_id'] = project_id if project_id | ||
| @http_client.get(path: '/stats', params: params) | ||
| end |
There was a problem hiding this comment.
Resources::Stats#get is a new API surface area but there are no specs covering the query param construction (from, to, optional project_id) or error propagation. Add a spec/lettermint/resources/stats_spec.rb to lock in the endpoint contract.
- Add Accept headers for non-JSON endpoints (messages source/html/text) - Add route_id/route_ids params to Stats#get per Issue #9 - Refactor build_params with compact, use consistently in domains - Add .to_sym to auth_headers for robustness - Fix YARD doc return type for TeamAPI#ping - Fix hash indentation in team_spec.rb - Add clarifying comment about Projects#find return type - Add comprehensive specs for webhooks, messages, projects, routes, suppressions, and stats resources (193 new examples) 446 examples, 0 failures
Summary
This PR introduces a major architectural enhancement that splits the SDK into two specialized API clients with distinct authentication schemes:
x-lettermint-tokenheader)Authorization: Bearerwithlm_team_*tokens)The existing
Clientclass becomes an alias toSendingAPI, maintaining full backward compatibility.Changes
New API Clients:
SendingAPIextracted from the originalClientimplementationTeamAPIfor team management with strictlm_team_*token validationHttpClientnow supports configurableauth_schemeparameterResource Classes (all inherit from
Resources::Base):Team- team settings, usage stats, member managementDomains- CRUD, DNS verification, project associationProjects- CRUD, token rotation, nested routes accessRoutes- transactional/broadcast route managementWebhooks- endpoint management with test/regenerate-secretMessages- history search with events/source/html/text sub-resourcesSuppressions- bounce/unsubscribe list managementStats- team and project-level statisticsInfrastructure:
Resources::Basewith shared pagination (page[size],page[cursor]) and filter helpersTest Coverage
253 examples, 0 failures across 5 new spec files:
sending_api_spec.rb- token validation, auth headers, HTTP methods, config, Client aliasteam_api_spec.rb- strictlm_team_*validation, Bearer auth, resource accessorsresources/base_spec.rb- pagination/sort/filter parameter buildingresources/team_spec.rb- all endpoint methodsresources/domains_spec.rb- full CRUD, DNS verification, project associationBackward Compatibility
Lettermint::Clientremains functional (alias toSendingAPI)Usage
Closes #9