Skip to content

Commit

Permalink
HttpApiClient.group & HttpApiClient.endpoint (#3977)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim <[email protected]>
  • Loading branch information
KhraksMamtsov and tim-smart authored Nov 26, 2024
1 parent 6c858a6 commit c963886
Show file tree
Hide file tree
Showing 8 changed files with 372 additions and 32 deletions.
7 changes: 7 additions & 0 deletions .changeset/chilled-mangos-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@effect/platform": patch
---

`HttpApiClient.group` & `HttpApiClient.endpoint` have been added
This makes it possible to create `HttpApiClient` for some part of the `HttpApi`
This eliminates the need to provide all the dependencies for the entire `HttpApi` - but only those necessary for its specific part to work
38 changes: 31 additions & 7 deletions packages/platform-node/test/HttpApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,42 @@ describe("HttpApi", () => {
describe("payload", () => {
it.effect("is decoded / encoded", () =>
Effect.gen(function*() {
const expected = new User({
id: 123,
name: "Joe",
createdAt: DateTime.unsafeMake(0)
})
const client = yield* HttpApiClient.make(Api)
const user = yield* client.users.create({
const clientUsersGroup = yield* HttpApiClient.group(Api, "users")
const clientUsersEndpointCreate = yield* HttpApiClient.endpoint(
Api,
"users",
"create"
)

const apiClientUser = yield* client.users.create({
urlParams: { id: 123 },
payload: { name: "Joe" }
})
assert.deepStrictEqual(
user,
new User({
id: 123,
name: "Joe",
createdAt: DateTime.unsafeMake(0)
})
apiClientUser,
expected
)
const groupClientUser = yield* clientUsersGroup.create({
urlParams: { id: 123 },
payload: { name: "Joe" }
})
assert.deepStrictEqual(
groupClientUser,
expected
)
const endpointClientUser = yield* clientUsersEndpointCreate({
urlParams: { id: 123 },
payload: { name: "Joe" }
})
assert.deepStrictEqual(
endpointClientUser,
expected
)
}).pipe(Effect.provide(HttpLive)))

Expand Down
127 changes: 127 additions & 0 deletions packages/platform/dtslint/HttpApiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware } from "@effect/platform"
import type { Schema } from "effect"
import { Effect } from "effect"

declare const ApiError: Schema.Schema<"ApiError", "ApiErrorEncoded", "ApiErrorR">

declare const Group1Error: Schema.Schema<"Group1Error", "Group1ErrorEncoded", "Group1ErrorR">
declare const EndpointAError: Schema.Schema<"EndpointAError", "EndpointAErrorEncoded", "EndpointAErrorR">
declare const EndpointASuccess: Schema.Schema<"EndpointASuccess", "EndpointASuccessEncoded", "EndpointASuccessR">
declare const EndpointBError: Schema.Schema<"EndpointBError", "EndpointBErrorEncoded", "EndpointBErrorR">
declare const EndpointBSuccess: Schema.Schema<"EndpointBSuccess", "EndpointBSuccessEncoded", "EndpointBSuccessR">

declare const EndpointASecurityError: Schema.Schema<
"EndpointASecurityError",
"EndpointASecurityErrorEncoded",
"EndpointASecurityErrorR"
>
class EndpointASecurity extends HttpApiMiddleware.Tag<EndpointASecurity>()("EndpointASecurity", {
failure: EndpointASecurityError
}) {}
declare const EndpointBSecurityError: Schema.Schema<
"EndpointBSecurityError",
"EndpointBSecurityErrorEncoded",
"EndpointBSecurityErrorR"
>
class EndpointBSecurity extends HttpApiMiddleware.Tag<EndpointBSecurity>()("EndpointBSecurity", {
failure: EndpointBSecurityError
}) {}
declare const Group1SecurityError: Schema.Schema<
"Group1SecurityError",
"Group1SecurityErrorEncoded",
"Group1SecurityErrorR"
>
class Group1Security extends HttpApiMiddleware.Tag<Group1Security>()("Group1Security", {
failure: Group1SecurityError
}) {}
declare const ApiSecurityError: Schema.Schema<
"ApiSecurityError",
"ApiSecurityErrorEncoded",
"ApiSecurityErrorR"
>
class ApiSecurity extends HttpApiMiddleware.Tag<ApiSecurity>()("ApiSecurity", {
failure: ApiSecurityError
}) {}

const EndpointA = HttpApiEndpoint.post("EndpointA", "/endpoint_a")
.middleware(EndpointASecurity)
.addError(EndpointAError)
.addSuccess(EndpointASuccess)
const EndpointB = HttpApiEndpoint.post("EndpointB", "/endpoint_b")
.middleware(EndpointBSecurity)
.addError(EndpointBError)
.addSuccess(EndpointBSuccess)

const Group1 = HttpApiGroup.make("Group1")
.middleware(Group1Security)
.addError(Group1Error)
.add(EndpointA)
.add(EndpointB)

declare const Group2Error: Schema.Schema<"Group2Error", "Group2ErrorEncoded", "Group2ErrorR">
declare const EndpointCError: Schema.Schema<"EndpointCError", "EndpointCErrorEncoded", "EndpointCErrorR">
declare const EndpointCSuccess: Schema.Schema<"EndpointCSuccess", "EndpointCSuccessEncoded", "EndpointCSuccessR">

const EndpointC = HttpApiEndpoint.post("EndpointC", "/endpoint_c")
.addError(EndpointCError)
.addSuccess(EndpointCSuccess)
const Group2 = HttpApiGroup.make("Group2")
.addError(Group2Error)
.add(EndpointC)

const TestApi = HttpApi.empty
.middleware(ApiSecurity)
.addError(ApiError)
.add(Group1)
.add(Group2)

// -------------------------------------------------------------------------------------
// HttpApiClient.endpoint
// -------------------------------------------------------------------------------------

Effect.gen(function*() {
const clientEndpointEffect = HttpApiClient.endpoint(TestApi, "Group1", "EndpointA")
// $ExpectType never
type _clientEndpointEffectError = Effect.Effect.Error<typeof clientEndpointEffect>
// $ExpectType "ApiErrorR" | "Group1ErrorR" | "EndpointAErrorR" | "EndpointASuccessR" | "EndpointASecurityErrorR" | "Group1SecurityErrorR" | "ApiSecurityErrorR" | HttpClient<HttpClientError, Scope>
type _clientEndpointEffectContext = Effect.Effect.Context<typeof clientEndpointEffect>

const clientEndpoint = yield* clientEndpointEffect

// $ExpectType Effect<"EndpointASuccess", "ApiError" | "Group1Error" | "EndpointAError" | "EndpointASecurityError" | "Group1SecurityError" | "ApiSecurityError" | HttpApiDecodeError | HttpClientError, never>
const _endpointCall = clientEndpoint({ withResponse: false })
})

// -------------------------------------------------------------------------------------
// HttpApiClient.group
// -------------------------------------------------------------------------------------

Effect.gen(function*() {
const clientGroupEffect = HttpApiClient.group(TestApi, "Group1")
// $ExpectType never
type _clientGroupEffectError = Effect.Effect.Error<typeof clientGroupEffect>
// $ExpectType "ApiErrorR" | "Group1ErrorR" | "EndpointAErrorR" | "EndpointASuccessR" | "EndpointBErrorR" | "EndpointBSuccessR" | "EndpointASecurityErrorR" | "EndpointBSecurityErrorR" | "Group1SecurityErrorR" | "ApiSecurityErrorR" | HttpClient<HttpClientError, Scope>
type _clientGroupEffectContext = Effect.Effect.Context<typeof clientGroupEffect>

const clientGroup = yield* clientGroupEffect

// $ExpectType Effect<"EndpointASuccess", "ApiError" | "Group1Error" | "EndpointAError" | "EndpointASecurityError" | "Group1SecurityError" | "ApiSecurityError" | HttpApiDecodeError | HttpClientError, never>
const _endpointCall = clientGroup.EndpointA({ withResponse: false })
})

// -------------------------------------------------------------------------------------
// HttpApiClient.make
// -------------------------------------------------------------------------------------

Effect.gen(function*() {
const clientApiEffect = HttpApiClient.make(TestApi)
// $ExpectType never
type _clientApiEffectError = Effect.Effect.Error<typeof clientApiEffect>
// $ExpectType "ApiErrorR" | "Group1ErrorR" | "EndpointAErrorR" | "EndpointASuccessR" | "EndpointBErrorR" | "EndpointBSuccessR" | "EndpointASecurityErrorR" | "EndpointBSecurityErrorR" | "Group1SecurityErrorR" | "ApiSecurityErrorR" | "Group2ErrorR" | "EndpointCErrorR" | "EndpointCSuccessR" | HttpClient<HttpClientError, Scope>
type _clientApiEffectContext = Effect.Effect.Context<typeof clientApiEffect>

const clientApi = yield* clientApiEffect

// $ExpectType Effect<"EndpointASuccess", "ApiError" | "Group1Error" | "EndpointAError" | "EndpointASecurityError" | "Group1SecurityError" | "ApiSecurityError" | HttpApiDecodeError | HttpClientError, never>
const _endpointCall = clientApi.Group1.EndpointA({ withResponse: false })
})
11 changes: 11 additions & 0 deletions packages/platform/src/HttpApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,10 @@ export const empty: HttpApi<never, HttpApiDecodeError> = makeProto({
export const reflect = <Groups extends HttpApiGroup.HttpApiGroup.Any, Error, R>(
self: HttpApi<Groups, Error, R>,
options: {
readonly predicate?: Predicate.Predicate<{
readonly endpoint: HttpApiEndpoint.HttpApiEndpoint.AnyWithProps
readonly group: HttpApiGroup.HttpApiGroup.AnyWithProps
}>
readonly onGroup: (options: {
readonly group: HttpApiGroup.HttpApiGroup.AnyWithProps
readonly mergedAnnotations: Context.Context<never>
Expand All @@ -294,6 +298,13 @@ export const reflect = <Groups extends HttpApiGroup.HttpApiGroup.Any, Error, R>(
})
const endpoints = Object.values(group.endpoints) as Iterable<HttpApiEndpoint.HttpApiEndpoint<string, HttpMethod>>
for (const endpoint of endpoints) {
if (
options.predicate && !options.predicate({
endpoint,
group
} as any)
) continue

const errors = extractMembers(endpoint.errorSchema.ast, groupErrors, HttpApiSchema.getStatusErrorAST)
options.onEndpoint({
group,
Expand Down
2 changes: 1 addition & 1 deletion packages/platform/src/HttpApiBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ export const group = <
HttpApiGroup.ApiGroup<Name>,
Handlers.Error<Return>,
| Handlers.Context<Return>
| HttpApiGroup.HttpApiGroup.ContextWithName<Groups, Name>
| HttpApiGroup.HttpApiGroup.MiddlewareWithName<Groups, Name>
> =>
Router.use((router) =>
Effect.gen(function*() {
Expand Down
Loading

0 comments on commit c963886

Please sign in to comment.